Implement simulation extensions

Squashed commit of the following:

commit 058f603c23accbcdbe9110645f19164da7d57c85
Author: Sampo Niskanen <sampo.niskanen@iki.fi>
Date:   Sat Apr 12 10:52:21 2014 +0300

    Implmenent simulation extensions

commit a1ca913c7d7793a9209a5f98235336270db6ce10
Author: Sampo Niskanen <sampo.niskanen@iki.fi>
Date:   Fri Apr 11 19:45:12 2014 +0300

    WIP

commit 916f0bc8961360c0906b413485ca4e3700033740
Author: Sampo Niskanen <sampo.niskanen@iki.fi>
Date:   Tue Apr 1 20:23:25 2014 +0300

    WIP
This commit is contained in:
Sampo Niskanen 2014-04-12 10:54:17 +03:00
parent 11328bc1fc
commit b6e3a57b47
29 changed files with 1593 additions and 194 deletions

View File

@ -377,11 +377,9 @@ simedtdlg.lbl.Timestep = Time step:
simedtdlg.lbl.ttip.Timestep1 = <html>The time between simulation steps.<br>A smaller time step results in a more accurate but slower simulation.<br> simedtdlg.lbl.ttip.Timestep1 = <html>The time between simulation steps.<br>A smaller time step results in a more accurate but slower simulation.<br>
simedtdlg.lbl.ttip.Timestep2 = The 4<sup>th</sup> order simulation method is quite accurate with a time step of simedtdlg.lbl.ttip.Timestep2 = The 4<sup>th</sup> order simulation method is quite accurate with a time step of
simedtdlg.but.ttip.resettodefault = Reset the time step to its default value ( simedtdlg.but.ttip.resettodefault = Reset the time step to its default value (
simedtdlg.border.Simlist = Simulator listeners simedtdlg.border.SimExt = Simulation extensions
simedtdlg.txt.longA1 = <html><i>Simulation listeners</i> is an advanced feature that allows user-written code to listen to and interact with the simulation. simedtdlg.SimExt.desc = <html><i>Simulation extensions</i> enable advanced features and custom functionality during flight simulations. You can for example do hardware-in-the-loop testing with them.
simedtdlg.txt.longA2 = For details on writing simulation listeners, see the OpenRocket technical documentation. simedtdlg.SimExt.noExtensions = No simulation extensions defined
simedtdlg.lbl.Curlist = Current listeners:
simedtdlg.lbl.Addsimlist = Add simulation listener
simedtdlg.lbl.Noflightdata = No flight data available. simedtdlg.lbl.Noflightdata = No flight data available.
simedtdlg.lbl.runsimfirst = Please run the simulation first. simedtdlg.lbl.runsimfirst = Please run the simulation first.
simedtdlg.chart.Simflight = Simulated flight simedtdlg.chart.Simflight = Simulated flight
@ -394,6 +392,12 @@ simedtdlg.IntensityDesc.High = High
simedtdlg.IntensityDesc.Veryhigh = Very high simedtdlg.IntensityDesc.Veryhigh = Very high
simedtdlg.IntensityDesc.Extreme = Extreme simedtdlg.IntensityDesc.Extreme = Extreme
SimulationExtension.airstart.name = Air-start ({alt})
SimulationExtension.javacode.name = Java code
SimulationExtension.javacode.name.none = none
SimulationExtension.javacode.desc = Add a custom SimulationListener to the simulation
SimulationExtension.javacode.className = Fully-qualified Java class name:
SimulationEditDialog.btn.plot = Plot SimulationEditDialog.btn.plot = Plot
SimulationEditDialog.btn.export = Export SimulationEditDialog.btn.export = Export
SimulationEditDialog.btn.edit = Edit SimulationEditDialog.btn.edit = Edit

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

View File

@ -23,7 +23,7 @@ import net.sf.openrocket.rocketcomponent.Rocket;
import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.FlightDataType;
import net.sf.openrocket.simulation.customexpression.CustomExpression; import net.sf.openrocket.simulation.customexpression.CustomExpression;
import net.sf.openrocket.simulation.listeners.SimulationListener; import net.sf.openrocket.simulation.extension.SimulationExtension;
import net.sf.openrocket.startup.Application; import net.sf.openrocket.startup.Application;
import net.sf.openrocket.util.ArrayList; import net.sf.openrocket.util.ArrayList;
@ -146,17 +146,8 @@ public class OpenRocketDocument implements ComponentChangeListener {
// simulation listeners // simulation listeners
for (Simulation sim : simulations) { for (Simulation sim : simulations) {
for (String className : sim.getSimulationListeners()) { for (SimulationExtension c : sim.getSimulationExtensions()) {
SimulationListener l = null; allTypes.addAll(c.getFlightDataTypes());
try {
Class<?> c = Class.forName(className);
l = (SimulationListener) c.newInstance();
Collections.addAll(allTypes, l.getFlightDataTypes());
//System.out.println("This document has expression datatype from "+l.getName());
} catch (Exception e) {
log.error("Could not instantiate listener: " + className);
}
} }
} }

View File

@ -21,7 +21,7 @@ import net.sf.openrocket.simulation.SimulationEngine;
import net.sf.openrocket.simulation.SimulationOptions; import net.sf.openrocket.simulation.SimulationOptions;
import net.sf.openrocket.simulation.SimulationStepper; import net.sf.openrocket.simulation.SimulationStepper;
import net.sf.openrocket.simulation.exception.SimulationException; import net.sf.openrocket.simulation.exception.SimulationException;
import net.sf.openrocket.simulation.exception.SimulationListenerException; import net.sf.openrocket.simulation.extension.SimulationExtension;
import net.sf.openrocket.simulation.listeners.SimulationListener; import net.sf.openrocket.simulation.listeners.SimulationListener;
import net.sf.openrocket.startup.Application; import net.sf.openrocket.startup.Application;
import net.sf.openrocket.util.ArrayList; import net.sf.openrocket.util.ArrayList;
@ -76,7 +76,8 @@ public class Simulation implements ChangeSource, Cloneable {
// TODO: HIGH: Change to use actual conditions class?? // TODO: HIGH: Change to use actual conditions class??
private SimulationOptions options; private SimulationOptions options;
private ArrayList<String> simulationListeners = new ArrayList<String>(); private ArrayList<SimulationExtension> simulationExtensions = new ArrayList<SimulationExtension>();
private final Class<? extends SimulationEngine> simulationEngineClass = BasicEventSimulationEngine.class; private final Class<? extends SimulationEngine> simulationEngineClass = BasicEventSimulationEngine.class;
private Class<? extends SimulationStepper> simulationStepperClass = RK4SimulationStepper.class; private Class<? extends SimulationStepper> simulationStepperClass = RK4SimulationStepper.class;
@ -116,7 +117,7 @@ public class Simulation implements ChangeSource, Cloneable {
public Simulation(Rocket rocket, Status status, String name, SimulationOptions options, public Simulation(Rocket rocket, Status status, String name, SimulationOptions options,
List<String> listeners, FlightData data) { List<SimulationExtension> extensions, FlightData data) {
if (rocket == null) if (rocket == null)
throw new IllegalArgumentException("rocket cannot be null"); throw new IllegalArgumentException("rocket cannot be null");
@ -142,8 +143,8 @@ public class Simulation implements ChangeSource, Cloneable {
this.options = options; this.options = options;
options.addChangeListener(new ConditionListener()); options.addChangeListener(new ConditionListener());
if (listeners != null) { if (extensions != null) {
this.simulationListeners.addAll(listeners); this.simulationExtensions.addAll(extensions);
} }
@ -196,14 +197,14 @@ public class Simulation implements ChangeSource, Cloneable {
/** /**
* Get the list of simulation listeners. The returned list is the one used by * Get the list of simulation extensions. The returned list is the one used by
* this object; changes to it will reflect changes in the simulation. * this object; changes to it will reflect changes in the simulation.
* *
* @return the actual list of simulation listeners. * @return the actual list of simulation extensions.
*/ */
public List<String> getSimulationListeners() { public List<SimulationExtension> getSimulationExtensions() {
mutex.verify(); mutex.verify();
return simulationListeners; return simulationExtensions;
} }
@ -293,16 +294,8 @@ public class Simulation implements ChangeSource, Cloneable {
simulationConditions.getSimulationListenerList().add(l); simulationConditions.getSimulationListenerList().add(l);
} }
for (String className : simulationListeners) { for (SimulationExtension extension : simulationExtensions) {
SimulationListener l = null; extension.initialize(simulationConditions);
try {
Class<?> c = Class.forName(className);
l = (SimulationListener) c.newInstance();
} catch (Exception e) {
throw new SimulationListenerException("Could not instantiate listener of " +
"class: " + className, e);
}
simulationConditions.getSimulationListenerList().add(l);
} }
long t1, t2; long t1, t2;
@ -410,7 +403,9 @@ public class Simulation implements ChangeSource, Cloneable {
copy.mutex = SafetyMutex.newInstance(); copy.mutex = SafetyMutex.newInstance();
copy.status = Status.NOT_SIMULATED; copy.status = Status.NOT_SIMULATED;
copy.options = this.options.clone(); copy.options = this.options.clone();
copy.simulationListeners = this.simulationListeners.clone(); for (SimulationExtension c : this.simulationExtensions) {
copy.simulationExtensions.add(c.clone());
}
copy.listeners = new ArrayList<EventListener>(); copy.listeners = new ArrayList<EventListener>();
copy.simulatedConditions = null; copy.simulatedConditions = null;
copy.simulatedConfiguration = null; copy.simulatedConfiguration = null;
@ -442,7 +437,9 @@ public class Simulation implements ChangeSource, Cloneable {
copy.name = this.name; copy.name = this.name;
copy.options.copyFrom(this.options); copy.options.copyFrom(this.options);
copy.simulatedConfiguration = this.simulatedConfiguration; copy.simulatedConfiguration = this.simulatedConfiguration;
copy.simulationListeners = this.simulationListeners.clone(); for (SimulationExtension c : this.simulationExtensions) {
copy.simulationExtensions.add(c.clone());
}
copy.simulationStepperClass = this.simulationStepperClass; copy.simulationStepperClass = this.simulationStepperClass;
copy.aerodynamicCalculatorClass = this.aerodynamicCalculatorClass; copy.aerodynamicCalculatorClass = this.aerodynamicCalculatorClass;

View File

@ -30,8 +30,10 @@ import net.sf.openrocket.simulation.FlightDataType;
import net.sf.openrocket.simulation.FlightEvent; import net.sf.openrocket.simulation.FlightEvent;
import net.sf.openrocket.simulation.SimulationOptions; import net.sf.openrocket.simulation.SimulationOptions;
import net.sf.openrocket.simulation.customexpression.CustomExpression; import net.sf.openrocket.simulation.customexpression.CustomExpression;
import net.sf.openrocket.simulation.extension.SimulationExtension;
import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.BuildProperties; import net.sf.openrocket.util.BuildProperties;
import net.sf.openrocket.util.Config;
import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.MathUtil;
import net.sf.openrocket.util.Reflection; import net.sf.openrocket.util.Reflection;
import net.sf.openrocket.util.TextUtil; import net.sf.openrocket.util.TextUtil;
@ -455,9 +457,18 @@ public class OpenRocketSaver extends RocketSaver {
indent--; indent--;
writeln("</conditions>"); writeln("</conditions>");
for (SimulationExtension extension : simulation.getSimulationExtensions()) {
for (String s : simulation.getSimulationListeners()) { Config config = extension.getConfig();
writeElement("listener", TextUtil.escapeXML(s)); writeln("<extension extensionid=\"" + TextUtil.escapeXML(extension.getId()) + "\">");
indent++;
if (config != null) {
for (String key : config.keySet()) {
Object value = config.get(key, null);
writeEntry(key, value);
}
}
indent--;
writeln("</extension>");
} }
// Write basic simulation data // Write basic simulation data
@ -512,6 +523,39 @@ public class OpenRocketSaver extends RocketSaver {
} }
private void writeEntry(String key, Object value) throws IOException {
if (value == null) {
return;
}
String keyAttr;
if (key != null) {
keyAttr = "key=\"" + key + "\" ";
} else {
keyAttr = "";
}
if (value instanceof Boolean) {
writeln("<entry " + keyAttr + "type=\"boolean\">" + value + "</entry>");
} else if (value instanceof Number) {
writeln("<entry " + keyAttr + "type=\"number\">" + value + "</entry>");
} else if (value instanceof String) {
writeln("<entry " + keyAttr + "type=\"string\">" + value + "</entry>");
} else if (value instanceof List) {
List<?> list = (List<?>) value;
writeln("<entry " + keyAttr + "type=\"list\">");
indent++;
for (Object o : list) {
writeEntry(null, o);
}
indent--;
writeln("</entry>");
} else {
// Unknown type
log.error("Unknown configuration value type " + value.getClass() + " value=" + value);
}
}
private void saveFlightDataBranch(FlightDataBranch branch, double timeSkip) private void saveFlightDataBranch(FlightDataBranch branch, double timeSkip)
throws IOException { throws IOException {
double previousTime = -100000; double previousTime = -100000;

View File

@ -0,0 +1,104 @@
package net.sf.openrocket.file.openrocket.importt;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.List;
import java.util.Random;
import net.sf.openrocket.aerodynamics.WarningSet;
import net.sf.openrocket.file.simplesax.AbstractElementHandler;
import net.sf.openrocket.file.simplesax.ElementHandler;
import net.sf.openrocket.file.simplesax.PlainTextHandler;
import net.sf.openrocket.util.ArrayList;
import net.sf.openrocket.util.Config;
import org.xml.sax.SAXException;
public class ConfigHandler extends AbstractElementHandler {
private ConfigHandler listHandler;
private Config config = new Config();
private List<Object> list = new ArrayList<Object>();
@Override
public ElementHandler openElement(String element, HashMap<String, String> attributes, WarningSet warnings) throws SAXException {
if (element.equals("entry") && "list".equals(attributes.get("type"))) {
listHandler = new ConfigHandler();
return listHandler;
} else {
return PlainTextHandler.INSTANCE;
}
}
@Override
public void closeElement(String element, HashMap<String, String> attributes, String content, WarningSet warnings) throws SAXException {
if (element.equals("entry")) {
String key = attributes.get("key");
String type = attributes.get("type");
Object value = null;
if ("boolean".equals(type)) {
value = Boolean.valueOf(content);
} else if ("string".equals(type)) {
value = content;
} else if ("number".equals(type)) {
value = parseNumber(content);
} else if ("list".equals(type)) {
value = listHandler.list;
}
if (value != null) {
if (key != null) {
config.put(key, value);
} else {
list.add(value);
}
}
} else {
super.closeElement(element, attributes, content, warnings);
}
}
private Number parseNumber(String str) {
try {
str = str.trim();
if (str.matches("^[+-]?[0-9]+$")) {
BigInteger value = new BigInteger(str, 10);
if (value.equals(BigInteger.valueOf(value.intValue()))) {
return value.intValue();
} else if (value.equals(BigInteger.valueOf(value.longValue()))) {
return value.longValue();
} else {
return value;
}
} else {
BigDecimal value = new BigDecimal(str);
if (value.equals(BigDecimal.valueOf(value.doubleValue()))) {
return value.doubleValue();
} else {
return value;
}
}
} catch (NumberFormatException e) {
return null;
}
}
public Config getConfig() {
return config;
}
public static void main(String[] args) {
Random rnd = new Random();
for (int i = 0; i < 1000000000; i++) {
double d = Double.longBitsToDouble(rnd.nextLong());
if (Double.isNaN(d))
continue;
String s = "" + d;
BigDecimal dec = new BigDecimal(s);
if (dec.doubleValue() != d) {
System.out.println("Fail: d=" + d + " dec=" + dec);
}
}
}
}

View File

@ -3,6 +3,7 @@ package net.sf.openrocket.file.openrocket.importt;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Set;
import net.sf.openrocket.aerodynamics.WarningSet; import net.sf.openrocket.aerodynamics.WarningSet;
import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.document.OpenRocketDocument;
@ -14,6 +15,13 @@ import net.sf.openrocket.file.simplesax.ElementHandler;
import net.sf.openrocket.file.simplesax.PlainTextHandler; import net.sf.openrocket.file.simplesax.PlainTextHandler;
import net.sf.openrocket.simulation.FlightData; import net.sf.openrocket.simulation.FlightData;
import net.sf.openrocket.simulation.SimulationOptions; import net.sf.openrocket.simulation.SimulationOptions;
import net.sf.openrocket.simulation.extension.SimulationExtension;
import net.sf.openrocket.simulation.extension.SimulationExtensionProvider;
import net.sf.openrocket.simulation.extension.impl.JavaCode;
import net.sf.openrocket.startup.Application;
import net.sf.openrocket.util.StringUtil;
import com.google.inject.Key;
class SingleSimulationHandler extends AbstractElementHandler { class SingleSimulationHandler extends AbstractElementHandler {
@ -24,9 +32,10 @@ class SingleSimulationHandler extends AbstractElementHandler {
private String name; private String name;
private SimulationConditionsHandler conditionHandler; private SimulationConditionsHandler conditionHandler;
private ConfigHandler configHandler;
private FlightDataHandler dataHandler; private FlightDataHandler dataHandler;
private final List<String> listeners = new ArrayList<String>(); private final List<SimulationExtension> extensions = new ArrayList<SimulationExtension>();
public SingleSimulationHandler(OpenRocketDocument doc, DocumentLoadingContext context) { public SingleSimulationHandler(OpenRocketDocument doc, DocumentLoadingContext context) {
this.doc = doc; this.doc = doc;
@ -47,6 +56,9 @@ class SingleSimulationHandler extends AbstractElementHandler {
} else if (element.equals("conditions")) { } else if (element.equals("conditions")) {
conditionHandler = new SimulationConditionsHandler(doc.getRocket(), context); conditionHandler = new SimulationConditionsHandler(doc.getRocket(), context);
return conditionHandler; return conditionHandler;
} else if (element.equals("extension")) {
configHandler = new ConfigHandler();
return configHandler;
} else if (element.equals("flightdata")) { } else if (element.equals("flightdata")) {
dataHandler = new FlightDataHandler(this, context); dataHandler = new FlightDataHandler(this, context);
return dataHandler; return dataHandler;
@ -71,7 +83,23 @@ class SingleSimulationHandler extends AbstractElementHandler {
warnings.add("Unknown calculator '" + content.trim() + "' specified, ignoring."); warnings.add("Unknown calculator '" + content.trim() + "' specified, ignoring.");
} }
} else if (element.equals("listener") && content.trim().length() > 0) { } else if (element.equals("listener") && content.trim().length() > 0) {
listeners.add(content.trim()); extensions.add(compatibilityExtension(content.trim()));
} else if (element.equals("extension") && !StringUtil.isEmpty(attributes.get("extensionid"))) {
String id = attributes.get("extensionid");
SimulationExtension extension = null;
Set<SimulationExtensionProvider> extensionProviders = Application.getInjector().getInstance(new Key<Set<SimulationExtensionProvider>>() {
});
for (SimulationExtensionProvider p : extensionProviders) {
if (p.getIds().contains(id)) {
extension = p.getInstance(id);
}
}
if (extension != null) {
extension.setConfig(configHandler.getConfig());
extensions.add(extension);
} else {
warnings.add("Simulation extension with id '" + id + "' not found.");
}
} }
} }
@ -105,8 +133,16 @@ class SingleSimulationHandler extends AbstractElementHandler {
data = dataHandler.getFlightData(); data = dataHandler.getFlightData();
Simulation simulation = new Simulation(doc.getRocket(), status, name, Simulation simulation = new Simulation(doc.getRocket(), status, name,
conditions, listeners, data); conditions, extensions, data);
doc.addSimulation(simulation); doc.addSimulation(simulation);
} }
private SimulationExtension compatibilityExtension(String className) {
JavaCode extension = Application.getInjector().getInstance(JavaCode.class);
extension.setClassName(className);
return extension;
}
} }

View File

@ -42,9 +42,8 @@ public class PluginModule extends AbstractModule {
if (c.isInterface()) if (c.isInterface())
continue; continue;
for (Class<?> intf : c.getInterfaces()) { for (Class<?> intf : interfaces) {
if (intf.isAssignableFrom(c)) {
if (interfaces.contains(intf)) {
// Ugly hack to enable dynamic binding... Can this be done type-safely? // Ugly hack to enable dynamic binding... Can this be done type-safely?
Multibinder<Object> binder = (Multibinder<Object>) findBinder(intf); Multibinder<Object> binder = (Multibinder<Object>) findBinder(intf);
binder.addBinding().to(c); binder.addBinding().to(c);

View File

@ -0,0 +1,100 @@
package net.sf.openrocket.simulation.extension;
import java.util.Collections;
import java.util.List;
import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.simulation.FlightDataType;
import net.sf.openrocket.util.AbstractChangeSource;
import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.Config;
import com.google.inject.Inject;
/**
* An abstract implementation of a SimulationExtension.
*/
public abstract class AbstractSimulationExtension extends AbstractChangeSource implements SimulationExtension, Cloneable {
@Inject
protected Translator trans;
protected Config config = new Config();
private final String name;
/**
* Use the current class name as the extension name. You should override
* getName if you use this constructor.
*/
protected AbstractSimulationExtension() {
this(null);
}
/**
* Use the provided name as a static name for this extension.
*/
protected AbstractSimulationExtension(String name) {
if (name != null) {
this.name = name;
} else {
this.name = this.getClass().getSimpleName();
}
}
/**
* By default, returns the canonical name of this class.
*/
@Override
public String getId() {
return this.getClass().getCanonicalName();
}
/**
* By default, returns the name provided to the constructor.
*/
@Override
public String getName() {
return name;
}
/**
* By default, returns null.
*/
@Override
public String getDescription() {
return null;
}
/**
* By default, returns an empty list.
*/
@Override
public List<FlightDataType> getFlightDataTypes() {
return Collections.emptyList();
}
/**
* By default, returns a new object obtained by calling Object.clone().
*/
@Override
public SimulationExtension clone() {
try {
AbstractSimulationExtension copy = (AbstractSimulationExtension) super.clone();
copy.config = this.config.clone();
return copy;
} catch (CloneNotSupportedException e) {
throw new BugException(e);
}
}
@Override
public Config getConfig() {
return config.clone();
}
@Override
public void setConfig(Config config) {
this.config = config.clone();
fireChangeEvent();
}
}

View File

@ -0,0 +1,65 @@
package net.sf.openrocket.simulation.extension;
import java.util.Arrays;
import java.util.List;
import net.sf.openrocket.l10n.Translator;
import com.google.inject.Inject;
import com.google.inject.Injector;
/**
* An abstract implementation of a SimulationExtensionProvider. The constructor is
* provided by the class of the SimulationExtension and the name of the extension.
*/
public abstract class AbstractSimulationExtensionProvider implements SimulationExtensionProvider {
@Inject
private Injector injector;
@Inject
protected Translator trans;
protected final Class<? extends SimulationExtension> extensionClass;
private final String[] name;
/**
* Sole constructor.
*
* @param extensionClass the simulation extension class
* @param name the name returned by getName
*/
protected AbstractSimulationExtensionProvider(Class<? extends SimulationExtension> extensionClass, String... name) {
this.extensionClass = extensionClass;
this.name = name;
}
/**
* By default returns the canonical name of the simulation extension class.
*/
@Override
public List<String> getIds() {
return Arrays.asList(extensionClass.getCanonicalName());
}
/**
* By default returns the provided extension name for the first ID that getIds returns.
*/
@Override
public List<String> getName(String id) {
if (id.equals(getIds().get(0))) {
return Arrays.asList(name);
}
return null;
}
/**
* By default returns a new instance of the simulation extension class instantiated by
* Class.newInstance.
*/
@Override
public SimulationExtension getInstance(String id) {
return injector.getInstance(extensionClass);
}
}

View File

@ -0,0 +1,81 @@
package net.sf.openrocket.simulation.extension;
import java.util.List;
import net.sf.openrocket.simulation.FlightDataType;
import net.sf.openrocket.simulation.SimulationConditions;
import net.sf.openrocket.simulation.exception.SimulationException;
import net.sf.openrocket.util.Config;
public interface SimulationExtension {
/**
* Return the simulation extension ID that is used when storing this
* extension to a file.
*
* @return the extension ID
*/
public String getId();
/**
* Return a short description of this extension. The name may contain
* elements from the extension's configuration, for example
* "Air start (150m)".
*
* @return a short name / description of this extension to be shown in the UI (must not be null)
*/
public String getName();
/**
* Return a longer description text for this extension, if available.
* This description may be shown in the UI as extra information about
* the extension.
*
* @return a longer description about this extension, or null if not available
*/
public String getDescription();
/**
* Initialize this simulation extension for running within a simulation.
* This method is called before running a simulation. It can either modify
* the simulation conditions or add simulation listeners to it.
*
* @param conditions the simulation conditions to be run
* @param configuration the extension configuration
*/
public void initialize(SimulationConditions conditions) throws SimulationException;
/**
* Return a list of any flight data types this simulation extension creates.
* This should only contain new types created by this extension, not existing
* types that the extension adds to the flight data.
*/
public List<FlightDataType> getFlightDataTypes();
/**
* Return a copy of this simulation extension, with all configuration deep-copied.
*
* @return a new copy of this simulation extension
*/
public SimulationExtension clone();
/**
* Return a Config object describing the current configuration of this simulation
* extension. The extension may keep its configuration in a Config object, or create
* it when requested.
*
* @return the simulation extension configuration.
*/
public Config getConfig();
/**
* Set this simulation extension's configuration. The extension should load all its
* configuration from the provided Config object.
*
* @param config the configuration to set
*/
public void setConfig(Config config);
}

View File

@ -0,0 +1,133 @@
package net.sf.openrocket.simulation.extension;
import java.util.HashMap;
import java.util.List;
import net.sf.openrocket.util.ArrayList;
/**
* A map containing simulation extension configuration. This map can
* store values of type int, long, float, double, boolean, String,
* List and SimulationExtensionConfiguration.
*/
public final class SimulationExtensionConfiguration extends HashMap<String, Object> {
private SimulationExtension extension;
public SimulationExtension getExtension() {
return extension;
}
public void setExtension(SimulationExtension extension) {
this.extension = extension;
}
@Override
public Object put(String key, Object value) {
Class<?> c = value.getClass();
if (c != Long.class && c != Integer.class &&
c != Double.class && c != Float.class &&
c != Boolean.class &&
!(value instanceof SimulationExtensionConfiguration) &&
!(value instanceof List)) {
throw new UnsupportedOperationException("Invalid configuration parameter type: " + c + " key=" + key + " value=" + value);
}
return super.put(key, value);
}
public long getLong(String key, long def) {
Object o = get(key);
if (o instanceof Number) {
return ((Number) o).longValue();
} else {
return def;
}
}
public int getInt(String key, int def) {
Object o = get(key);
if (o instanceof Number) {
return ((Number) o).intValue();
} else {
return def;
}
}
public double getDouble(String key, double def) {
Object o = get(key);
if (o instanceof Number) {
return ((Number) o).doubleValue();
} else {
return def;
}
}
public float getFloat(String key, float def) {
Object o = get(key);
if (o instanceof Number) {
return ((Number) o).floatValue();
} else {
return def;
}
}
public boolean getBoolean(String key, boolean def) {
Object o = get(key);
if (o instanceof Boolean) {
return (Boolean) o;
} else {
return def;
}
}
public String getString(String key, String def) {
Object o = get(key);
if (o instanceof String) {
return (String) o;
} else {
return def;
}
}
/**
* Deep-clone this object.
*/
@Override
public SimulationExtensionConfiguration clone() {
SimulationExtensionConfiguration copy = new SimulationExtensionConfiguration();
copy.extension = this.extension;
for (String key : this.keySet()) {
Object value = this.get(key);
if (value instanceof SimulationExtensionConfiguration) {
copy.put(key, ((SimulationExtensionConfiguration) value).clone());
} else if (value instanceof List) {
copy.put(key, cloneList((List<?>) value));
} else {
copy.put(key, value);
}
}
return copy;
}
private Object cloneList(List<?> original) {
ArrayList<Object> list = new ArrayList<Object>();
for (Object value : original) {
if (value instanceof SimulationExtensionConfiguration) {
list.add(((SimulationExtensionConfiguration) value).clone());
} else if (value instanceof List) {
list.add(cloneList((List<?>) value));
} else {
list.add(value);
}
}
return list;
}
}

View File

@ -0,0 +1,46 @@
package net.sf.openrocket.simulation.extension;
import java.util.List;
import net.sf.openrocket.plugin.Plugin;
@Plugin
public interface SimulationExtensionProvider {
/**
* Return a list of simulation extension ID's that this provider supports.
* The ID is used to identify the plugin when storing files. It should follow
* the conventions of Java package and class naming.
*
* @return a list of ID strings
*/
public List<String> getIds();
/**
* Return the UI name for a simulation extension. The first values
* are nested menus, with the last one the actual entry, for example
* ["Launch conditions", "Air-start"].
*
* If the ID does not represent an extension that should be displayed
* in the UI, this method must return null. For example, if an extension
* has multiple ID's, this method must return the menu name for only one
* of the ID's.
*
* These can be localized, and the system may attempt to localize
* English-language names automatically (mainly for the menus).
*
* @param id the extension ID
* @return the UI name for the extension, or null for no display
*/
public List<String> getName(String id);
/**
* Return a new instance of a simulation extension. This is a new instance
* that should have some default configuration.
*
* @param id the extension ID
* @return a new simulation extension instance
*/
public SimulationExtension getInstance(String id);
}

View File

@ -0,0 +1,41 @@
package net.sf.openrocket.simulation.extension.impl;
import net.sf.openrocket.l10n.L10N;
import net.sf.openrocket.simulation.SimulationConditions;
import net.sf.openrocket.simulation.SimulationStatus;
import net.sf.openrocket.simulation.exception.SimulationException;
import net.sf.openrocket.simulation.extension.AbstractSimulationExtension;
import net.sf.openrocket.simulation.listeners.AbstractSimulationListener;
import net.sf.openrocket.unit.UnitGroup;
import net.sf.openrocket.util.Coordinate;
public class AirStart extends AbstractSimulationExtension {
@Override
public void initialize(SimulationConditions conditions) throws SimulationException {
conditions.getSimulationListenerList().add(new AirStartListener());
}
@Override
public String getName() {
String name = trans.get("SimulationExtension.airstart.name");
return L10N.replace(name, "{alt}", UnitGroup.UNITS_DISTANCE.toStringUnit(getLaunchAltitude()));
}
public double getLaunchAltitude() {
return config.getDouble("launchAltitude", 0.0);
}
public void setLaunchAltitude(double launchAltitude) {
config.put("launchAltitude", launchAltitude);
fireChangeEvent();
}
private class AirStartListener extends AbstractSimulationListener {
@Override
public void startSimulation(SimulationStatus status) throws SimulationException {
status.setRocketPosition(new Coordinate(0, 0, getLaunchAltitude()));
}
}
}

View File

@ -0,0 +1,13 @@
package net.sf.openrocket.simulation.extension.impl;
import net.sf.openrocket.plugin.Plugin;
import net.sf.openrocket.simulation.extension.AbstractSimulationExtensionProvider;
@Plugin
public class AirStartProvider extends AbstractSimulationExtensionProvider {
public AirStartProvider() {
super(AirStart.class, "Launch conditions", "Air-start");
}
}

View File

@ -0,0 +1,77 @@
package net.sf.openrocket.simulation.extension.impl;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;
import net.sf.openrocket.simulation.SimulationConditions;
import net.sf.openrocket.simulation.exception.SimulationException;
import net.sf.openrocket.simulation.extension.AbstractSimulationExtension;
import net.sf.openrocket.simulation.listeners.SimulationListener;
import net.sf.openrocket.util.ArrayList;
import net.sf.openrocket.util.StringUtil;
import com.google.inject.Inject;
import com.google.inject.Injector;
public class JavaCode extends AbstractSimulationExtension {
@Inject
private Injector injector;
public JavaCode() {
config.put("my_string", "foobar");
config.put("my_int", 123);
config.put("my_long", 123456789012345L);
config.put("my_float", 12.345f);
config.put("my_double", 12.345e99);
config.put("my_bigint", new BigInteger("12345678901234567890", 10));
config.put("my_bool", true);
List<Object> list = new ArrayList<Object>();
list.add(true);
list.add(123);
list.add(123.456);
list.add(Arrays.asList(1, 2, 3));
list.add("foo");
config.put("my_list", list);
}
@Override
public void initialize(SimulationConditions conditions) throws SimulationException {
String className = getClassName();
try {
if (!StringUtil.isEmpty(className)) {
Class<?> clazz = Class.forName(className);
if (!SimulationListener.class.isAssignableFrom(clazz)) {
throw new SimulationException("Class " + className + " does not implement SimulationListener");
}
SimulationListener listener = (SimulationListener) injector.getInstance(clazz);
conditions.getSimulationListenerList().add(listener);
}
} catch (ClassNotFoundException e) {
throw new SimulationException("Could not find class " + className);
}
}
@Override
public String getName() {
String name = trans.get("SimulationExtension.javacode.name") + ": ";
String className = getClassName();
if (!StringUtil.isEmpty(className)) {
name = name + className;
} else {
name = name + trans.get("SimulationExtension.javacode.name.none");
}
return name;
}
public String getClassName() {
return config.getString("className", "");
}
public void setClassName(String className) {
config.put("className", className);
fireChangeEvent();
}
}

View File

@ -0,0 +1,13 @@
package net.sf.openrocket.simulation.extension.impl;
import net.sf.openrocket.plugin.Plugin;
import net.sf.openrocket.simulation.extension.AbstractSimulationExtensionProvider;
@Plugin
public class JavaCodeProvider extends AbstractSimulationExtensionProvider {
public JavaCodeProvider() {
super(JavaCode.class, "User code", "Java code");
}
}

View File

@ -0,0 +1,152 @@
package net.sf.openrocket.util;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
public class Config {
private LinkedHashMap<String, Object> map = new LinkedHashMap<String, Object>();
public void put(String key, String value) {
validateType(value);
map.put(key, value);
}
public void put(String key, Number value) {
validateType(value);
map.put(key, clone(value));
}
public void put(String key, Boolean value) {
validateType(value);
map.put(key, value);
}
public void put(String key, List<?> value) {
validateType(value);
map.put(key, clone(value));
}
public void put(String key, Object value) {
validateType(value);
map.put(key, clone(value));
}
public Object get(String key, Object def) {
return get(key, def, Object.class);
}
public Boolean getBoolean(String key, Boolean def) {
return get(key, def, Boolean.class);
}
public Integer getInt(String key, Integer def) {
Number number = get(key, null, Number.class);
if (number == null) {
return def;
} else {
return number.intValue();
}
}
public Long getLong(String key, Long def) {
Number number = get(key, null, Number.class);
if (number == null) {
return def;
} else {
return number.longValue();
}
}
public Double getDouble(String key, Double def) {
Number number = get(key, null, Number.class);
if (number == null) {
return def;
} else {
return number.doubleValue();
}
}
public String getString(String key, String def) {
return get(key, def, String.class);
}
public List<?> getList(String key, List<?> def) {
return get(key, def, List.class);
}
public boolean containsKey(String key) {
return map.containsKey(key);
}
public Set<String> keySet() {
return Collections.unmodifiableMap(map).keySet();
}
@Override
public Config clone() {
Config copy = new Config();
for (Entry<String, Object> entry : map.entrySet()) {
copy.map.put(entry.getKey(), clone(entry.getValue()));
}
return copy;
}
@SuppressWarnings("unchecked")
private <T> T get(String key, T def, Class<T> type) {
Object value = map.get(key);
if (type.isInstance(value)) {
return (T) value;
} else {
return def;
}
}
private void validateType(Object value) {
if (value == null) {
throw new NullPointerException("Attempting to add null value to Config object");
} else if (value instanceof Boolean) {
// ok
} else if (value instanceof Number) {
// ok
} else if (value instanceof String) {
// ok
} else if (value instanceof List<?>) {
List<?> list = (List<?>) value;
for (Object v : list) {
validateType(v);
}
} else {
throw new IllegalArgumentException("Attempting to add value of type " + value.getClass() + " to Config object, value=" + value);
}
}
private Object clone(Object value) {
if (value instanceof Byte || value instanceof Short || value instanceof Integer || value instanceof Long ||
value instanceof Float || value instanceof Double || value instanceof Boolean || value instanceof String) {
// immutable
return value;
} else if (value instanceof Number) {
return new BigDecimal(value.toString());
} else if (value instanceof List<?>) {
List<?> list = (List<?>) value;
ArrayList<Object> copy = new ArrayList<Object>(list.size());
for (Object o : list) {
copy.add(clone(o));
}
return copy;
} else {
throw new IllegalStateException("Config contained value = " + value + " type = " + ((value != null) ? value.getClass() : "null"));
}
}
}

View File

@ -0,0 +1,164 @@
package net.sf.openrocket.util;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
public class TestConfig {
private Config config = new Config();
@Test
public void testDoubles() {
config.put("double", Math.PI);
config.put("bigdecimal", new BigDecimal(Math.PI));
assertEquals(Math.PI, config.getDouble("double", null), 0);
assertEquals(Math.PI, config.getDouble("bigdecimal", null), 0);
assertEquals(3, (int) config.getInt("double", null));
}
@Test
public void testInts() {
config.put("int", 123);
config.put("biginteger", new BigDecimal(Math.PI));
config.put("bigdecimal", new BigDecimal(Math.PI));
assertEquals(123, (int) config.getInt("int", null));
assertEquals(3, (int) config.getInt("bigdecimal", null));
assertEquals(3, (int) config.getInt("biginteger", null));
}
@Test
public void testDefaultValue() {
assertEquals(true, config.getBoolean("foo", true));
assertEquals(123, (int) config.getInt("foo", 123));
assertEquals(123L, (long) config.getLong("foo", 123L));
assertEquals(1.23, (double) config.getDouble("foo", 1.23), 0);
assertEquals("bar", config.getString("foo", "bar"));
assertEquals(Arrays.asList("foo"), config.getList("foo", Arrays.asList("foo")));
}
@Test
public void testNullDefaultValue() {
assertEquals(null, config.getBoolean("foo", null));
assertEquals(null, config.getInt("foo", null));
assertEquals(null, config.getLong("foo", null));
assertEquals(null, config.getDouble("foo", null));
assertEquals(null, config.getString("foo", null));
assertEquals(null, config.getList("foo", null));
}
@Test
public void testStoringList() {
List<Object> list = new ArrayList<Object>();
list.add("Foo");
list.add(123);
list.add(Math.PI);
list.add(true);
config.put("list", list);
assertEquals(Arrays.asList("Foo", 123, Math.PI, true), config.getList("list", null));
}
@Test
public void testModifyingStoredList() {
List<Object> list = new ArrayList<Object>();
list.add("Foo");
list.add(123);
list.add(Math.PI);
list.add(true);
config.put("list", list);
list.add("hello");
assertEquals(Arrays.asList("Foo", 123, Math.PI, true), config.getList("list", null));
}
@Test
public void testModifyingStoredNumber() {
AtomicInteger ai = new AtomicInteger(100);
config.put("ai", ai);
ai.incrementAndGet();
assertEquals(100, (int) config.getInt("ai", null));
}
@Test
public void testClone() {
config.put("string", "foo");
config.put("int", 123);
config.put("double", Math.PI);
AtomicInteger ai = new AtomicInteger(100);
config.put("atomicinteger", ai);
List<Object> list = new ArrayList<Object>();
list.add("Foo");
config.put("list", list);
Config copy = config.clone();
config.put("extra", "foo");
ai.incrementAndGet();
assertFalse(copy.containsKey("extra"));
assertEquals("foo", copy.getString("string", null));
assertEquals(123, (int) copy.getInt("int", null));
assertEquals(100, (int) copy.getInt("atomicinteger", null));
assertEquals(Math.PI, (double) copy.getDouble("double", null), 0);
assertEquals(Arrays.asList("Foo"), copy.getList("list", null));
}
@Test
public void testStoringNullValue() {
try {
config.put("foo", (Boolean) null);
fail();
} catch (NullPointerException e) {
}
try {
config.put("foo", (String) null);
fail();
} catch (NullPointerException e) {
}
try {
config.put("foo", (Number) null);
fail();
} catch (NullPointerException e) {
}
try {
config.put("foo", (List<?>) null);
fail();
} catch (NullPointerException e) {
}
}
@Test
public void testStoringListWithInvalidTypes() {
List<Object> list = new ArrayList<Object>();
list.add("Foo");
list.add(new Date());
try {
config.put("foo", list);
fail();
} catch (IllegalArgumentException e) {
}
}
@Test
public void testStoringListWithNull() {
List<Object> list = new ArrayList<Object>();
list.add("Foo");
list.add(new Date());
try {
config.put("foo", list);
fail();
} catch (IllegalArgumentException e) {
}
}
}

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<classpath> <classpath>
<classpathentry kind="src" path="src"/> <classpathentry kind="src" path="src"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/java-6-openjdk-amd64"/>
<classpathentry kind="lib" path="lib/jogl/jogl-all.jar"/> <classpathentry kind="lib" path="lib/jogl/jogl-all.jar"/>
<classpathentry kind="lib" path="lib/iText-5.0.2.jar"/> <classpathentry kind="lib" path="lib/iText-5.0.2.jar"/>
<classpathentry kind="lib" path="lib/jcommon-1.0.18.jar"/> <classpathentry kind="lib" path="lib/jcommon-1.0.18.jar"/>

View File

@ -5,7 +5,15 @@ org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonN
org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.6
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.autoboxing=ignore org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
org.eclipse.jdt.core.compiler.problem.comparingIdentical=error org.eclipse.jdt.core.compiler.problem.comparingIdentical=error
org.eclipse.jdt.core.compiler.problem.deadCode=warning org.eclipse.jdt.core.compiler.problem.deadCode=warning
@ -14,6 +22,7 @@ org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
org.eclipse.jdt.core.compiler.problem.discouragedReference=warning org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
org.eclipse.jdt.core.compiler.problem.fallthroughCase=error org.eclipse.jdt.core.compiler.problem.fallthroughCase=error
org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled
@ -34,7 +43,7 @@ org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore
org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore
org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning org.eclipse.jdt.core.compiler.problem.missingSerialVersion=ignore
org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
org.eclipse.jdt.core.compiler.problem.noEffectAssignment=error org.eclipse.jdt.core.compiler.problem.noEffectAssignment=error
org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
@ -87,3 +96,4 @@ org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore
org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
org.eclipse.jdt.core.compiler.source=1.6

View File

@ -26,6 +26,7 @@ import net.sf.openrocket.gui.util.GUIUtil;
import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.rocketcomponent.Configuration; import net.sf.openrocket.rocketcomponent.Configuration;
import net.sf.openrocket.simulation.SimulationOptions; import net.sf.openrocket.simulation.SimulationOptions;
import net.sf.openrocket.simulation.extension.SimulationExtension;
import net.sf.openrocket.startup.Application; import net.sf.openrocket.startup.Application;
@ -91,8 +92,10 @@ public class SimulationEditDialog extends JDialog {
if (simulation.length > 1) { if (simulation.length > 1) {
for (int i = 1; i < simulation.length; i++) { for (int i = 1; i < simulation.length; i++) {
simulation[i].getOptions().copyConditionsFrom(simulation[0].getOptions()); simulation[i].getOptions().copyConditionsFrom(simulation[0].getOptions());
simulation[i].getSimulationListeners().clear(); simulation[i].getSimulationExtensions().clear();
simulation[i].getSimulationListeners().addAll(simulation[0].getSimulationListeners()); for (SimulationExtension c : simulation[0].getSimulationExtensions()) {
simulation[i].getSimulationExtensions().add(c.clone());
}
} }
} }
} }

View File

@ -1,21 +1,26 @@
package net.sf.openrocket.gui.simulation; package net.sf.openrocket.gui.simulation;
import java.awt.Component; import java.awt.Color;
import java.awt.Dialog.ModalityType;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import java.util.Arrays; import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.swing.AbstractListModel;
import javax.swing.BorderFactory; import javax.swing.BorderFactory;
import javax.swing.JButton; import javax.swing.JButton;
import javax.swing.JComboBox; import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel; import javax.swing.JLabel;
import javax.swing.JList; import javax.swing.JMenu;
import javax.swing.JOptionPane; import javax.swing.JMenuItem;
import javax.swing.JPanel; import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane; import javax.swing.JScrollPane;
import javax.swing.JSpinner; import javax.swing.JSpinner;
import javax.swing.ListCellRenderer; import javax.swing.MenuElement;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
@ -25,23 +30,31 @@ import net.sf.openrocket.gui.adaptors.DoubleModel;
import net.sf.openrocket.gui.adaptors.EnumModel; import net.sf.openrocket.gui.adaptors.EnumModel;
import net.sf.openrocket.gui.components.BasicSlider; import net.sf.openrocket.gui.components.BasicSlider;
import net.sf.openrocket.gui.components.DescriptionArea; import net.sf.openrocket.gui.components.DescriptionArea;
import net.sf.openrocket.gui.components.StyledLabel;
import net.sf.openrocket.gui.components.StyledLabel.Style;
import net.sf.openrocket.gui.components.UnitSelector; import net.sf.openrocket.gui.components.UnitSelector;
import net.sf.openrocket.gui.util.GUIUtil;
import net.sf.openrocket.gui.util.Icons; import net.sf.openrocket.gui.util.Icons;
import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.simulation.RK4SimulationStepper; import net.sf.openrocket.simulation.RK4SimulationStepper;
import net.sf.openrocket.simulation.SimulationOptions; import net.sf.openrocket.simulation.SimulationOptions;
import net.sf.openrocket.simulation.listeners.SimulationListener; import net.sf.openrocket.simulation.extension.SimulationExtension;
import net.sf.openrocket.simulation.listeners.example.CSVSaveListener; import net.sf.openrocket.simulation.extension.SimulationExtensionProvider;
import net.sf.openrocket.simulation.extension.SwingSimulationExtensionConfigurator;
import net.sf.openrocket.startup.Application; import net.sf.openrocket.startup.Application;
import net.sf.openrocket.unit.UnitGroup; import net.sf.openrocket.unit.UnitGroup;
import net.sf.openrocket.util.GeodeticComputationStrategy; import net.sf.openrocket.util.GeodeticComputationStrategy;
import com.google.inject.Key;
class SimulationOptionsPanel extends JPanel { class SimulationOptionsPanel extends JPanel {
private static final Translator trans = Application.getTranslator(); private static final Translator trans = Application.getTranslator();
final Simulation simulation; final Simulation simulation;
private JPanel currentExtensions;
SimulationOptionsPanel(final Simulation simulation) { SimulationOptionsPanel(final Simulation simulation) {
super(new MigLayout("fill")); super(new MigLayout("fill"));
this.simulation = simulation; this.simulation = simulation;
@ -162,133 +175,255 @@ class SimulationOptionsPanel extends JPanel {
//// Simulation listeners //// Simulation extensions
sub = new JPanel(new MigLayout("fill, gap 0 0")); sub = new JPanel(new MigLayout("fill, gap 0 0"));
//// Simulator listeners sub.setBorder(BorderFactory.createTitledBorder(trans.get("simedtdlg.border.SimExt")));
sub.setBorder(BorderFactory.createTitledBorder(trans.get("simedtdlg.border.Simlist"))); this.add(sub, "wmin 300lp, growx, growy");
this.add(sub, "growx, growy");
DescriptionArea desc = new DescriptionArea(5); DescriptionArea desc = new DescriptionArea(5);
//// <html><i>Simulation listeners</i> is an advanced feature that allows user-written code to listen to and interact with the simulation. desc.setText(trans.get("simedtdlg.SimExt.desc"));
//// For details on writing simulation listeners, see the OpenRocket technical documentation. sub.add(desc, "aligny 0, hmin 100lp, growx, wrap para");
desc.setText(trans.get("simedtdlg.txt.longA1") +
trans.get("simedtdlg.txt.longA2"));
sub.add(desc, "aligny 0, growx, wrap para");
//// Current listeners:
label = new JLabel(trans.get("simedtdlg.lbl.Curlist"));
sub.add(label, "spanx, wrap rel");
final ListenerListModel listenerModel = new ListenerListModel(); final JButton addExtension = new JButton("Add extension");
final JList list = new JList(listenerModel); final JPopupMenu menu = getExtensionMenu();
list.setCellRenderer(new ListenerCellRenderer()); addExtension.addActionListener(new ActionListener() {
JScrollPane scroll = new JScrollPane(list); public void actionPerformed(ActionEvent ev) {
// scroll.setPreferredSize(new Dimension(1,1)); menu.show(addExtension, 5, addExtension.getBounds().height);
sub.add(scroll, "height 1px, grow, wrap rel");
//// Add button
button = new JButton(trans.get("simedtdlg.but.add"));
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String previous = Application.getPreferences().getString("previousListenerName", "");
String input = (String) JOptionPane.showInputDialog(SwingUtilities.getRoot(SimulationOptionsPanel.this),
new Object[] {
//// Type the full Java class name of the simulation listener, for example:
"Type the full Java class name of the simulation listener, for example:",
"<html><tt>" + CSVSaveListener.class.getName() + "</tt>" },
//// Add simulation listener
trans.get("simedtdlg.lbl.Addsimlist"),
JOptionPane.QUESTION_MESSAGE,
null, null,
previous
);
if (input == null || input.equals(""))
return;
Application.getPreferences().putString("previousListenerName", input);
simulation.getSimulationListeners().add(input);
listenerModel.fireContentsChanged();
} }
}); });
sub.add(button, "split 2, sizegroup buttons, alignx 50%, gapright para"); sub.add(addExtension, "growx, wrap 0");
//// Remove button currentExtensions = new JPanel(new MigLayout("fillx, gap 0 0, ins 0"));
button = new JButton(trans.get("simedtdlg.but.remove")); JScrollPane scroll = new JScrollPane(currentExtensions);
button.addActionListener(new ActionListener() { // &#$%! scroll pane will not honor "growy"...
@Override sub.add(scroll, "growx, growy, h 100%");
public void actionPerformed(ActionEvent e) {
int[] selected = list.getSelectedIndices(); updateCurrentExtensions();
Arrays.sort(selected);
for (int i = selected.length - 1; i >= 0; i--) { }
simulation.getSimulationListeners().remove(selected[i]);
private JPopupMenu getExtensionMenu() {
Set<SimulationExtensionProvider> extensions = Application.getInjector().getInstance(new Key<Set<SimulationExtensionProvider>>() {
});
JPopupMenu basemenu = new JPopupMenu();
for (final SimulationExtensionProvider provider : extensions) {
List<String> ids = provider.getIds();
for (final String id : ids) {
List<String> menuItems = provider.getName(id);
if (menuItems != null) {
JComponent menu = findMenu(basemenu, menuItems);
JMenuItem item = new JMenuItem(menuItems.get(menuItems.size() - 1));
item.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
SimulationExtension e = provider.getInstance(id);
simulation.getSimulationExtensions().add(e);
updateCurrentExtensions();
}
});
menu.add(item);
} }
listenerModel.fireContentsChanged();
} }
}); }
sub.add(button, "sizegroup buttons, alignx 50%"); return basemenu;
} }
private class ListenerCellRenderer extends JLabel implements ListCellRenderer { private JComponent findMenu(MenuElement menu, List<String> menuItems) {
for (int i = 0; i < menuItems.size() - 1; i++) {
String menuItem = menuItems.get(i);
@Override MenuElement found = null;
public Component getListCellRendererComponent(JList list, Object value, for (MenuElement e : menu.getSubElements()) {
int index, boolean isSelected, boolean cellHasFocus) { if (e instanceof JMenu && ((JMenu) e).getText().equals(menuItem)) {
String s = value.toString(); found = e;
setText(s); break;
}
// Attempt instantiating, catch any exceptions
Exception ex = null;
try {
Class<?> c = Class.forName(s);
@SuppressWarnings("unused")
SimulationListener l = (SimulationListener) c.newInstance();
} catch (Exception e) {
ex = e;
} }
if (ex == null) { if (found != null) {
setIcon(Icons.SIMULATION_LISTENER_OK); menu = found;
//// Listener instantiated successfully.
setToolTipText("Listener instantiated successfully.");
} else { } else {
setIcon(Icons.SIMULATION_LISTENER_ERROR); JMenu m = new JMenu(menuItem);
//// <html>Unable to instantiate listener due to exception:<br> ((JComponent) menu).add(m);
setToolTipText("<html>Unable to instantiate listener due to exception:<br>" + menu = m;
ex.toString()); }
}
return (JComponent) menu;
}
private void updateCurrentExtensions() {
currentExtensions.removeAll();
if (simulation.getSimulationExtensions().isEmpty()) {
StyledLabel l = new StyledLabel(trans.get("simedtdlg.SimExt.noExtensions"), Style.ITALIC);
l.setForeground(Color.DARK_GRAY);
currentExtensions.add(l, "growx, pad 5 5 5 5, wrap");
} else {
for (SimulationExtension e : simulation.getSimulationExtensions()) {
currentExtensions.add(new SimulationExtensionPanel(e), "growx, wrap");
}
}
// Both needed:
this.revalidate();
this.repaint();
}
private class SimulationExtensionPanel extends JPanel {
public SimulationExtensionPanel(final SimulationExtension extension) {
super(new MigLayout("fillx, gapx 0"));
this.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY));
this.add(new JLabel(extension.getName()), "spanx, growx, wrap");
JButton button;
this.add(new JPanel(), "spanx, split, growx, right");
if (findConfigurator(extension) != null) {
button = new JButton(Icons.CONFIGURE);
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
findConfigurator(extension).configure(extension, simulation,
SwingUtilities.windowForComponent(SimulationOptionsPanel.this));
updateCurrentExtensions();
}
});
this.add(button, "right");
} }
if (isSelected) { if (extension.getDescription() != null) {
setBackground(list.getSelectionBackground()); button = new JButton(Icons.HELP);
setForeground(list.getSelectionForeground()); button.addActionListener(new ActionListener() {
} else { @Override
setBackground(list.getBackground()); public void actionPerformed(ActionEvent e) {
setForeground(list.getForeground()); final JDialog dialog = new JDialog(SwingUtilities.windowForComponent(SimulationOptionsPanel.this),
extension.getName(), ModalityType.APPLICATION_MODAL);
JPanel panel = new JPanel(new MigLayout("fill"));
DescriptionArea area = new DescriptionArea(extension.getDescription(), 10, 0);
panel.add(area, "width 400lp, wrap para");
JButton close = new JButton(trans.get("button.close"));
close.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
dialog.setVisible(false);
}
});
panel.add(close, "right");
dialog.add(panel);
GUIUtil.setDisposableDialogOptions(dialog, close);
dialog.setLocationRelativeTo(SwingUtilities.windowForComponent(SimulationOptionsPanel.this));
dialog.setVisible(true);
}
});
this.add(button, "right");
} }
setOpaque(true);
return this; button = new JButton(Icons.DELETE);
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
Iterator<SimulationExtension> iter = simulation.getSimulationExtensions().iterator();
while (iter.hasNext()) {
// Compare with identity
if (iter.next() == extension) {
iter.remove();
break;
}
}
updateCurrentExtensions();
}
});
this.add(button, "right");
}
private SwingSimulationExtensionConfigurator findConfigurator(SimulationExtension extension) {
Set<SwingSimulationExtensionConfigurator> configurators = Application.getInjector().getInstance(new Key<Set<SwingSimulationExtensionConfigurator>>() {
});
for (SwingSimulationExtensionConfigurator c : configurators) {
if (c.support(extension)) {
return c;
}
}
return null;
} }
} }
private class ListenerListModel extends AbstractListModel { //
@Override //
public String getElementAt(int index) { // private class ExtensionListModel extends AbstractListModel {
if (index < 0 || index >= getSize()) // @Override
return null; // public SimulationExtensionConfiguration getElementAt(int index) {
return simulation.getSimulationListeners().get(index); // if (index < 0 || index >= getSize())
} // return null;
// return simulation.getSimulationExtensions().get(index);
@Override // }
public int getSize() { //
return simulation.getSimulationListeners().size(); // @Override
} // public int getSize() {
// return simulation.getSimulationExtensions().size();
public void fireContentsChanged() { // }
super.fireContentsChanged(this, 0, getSize()); // }
} //
} //
// private class ExtensionCellRenderer extends JPanel implements ListCellRenderer {
// private JLabel label;
//
// public ExtensionCellRenderer() {
// super(new MigLayout("fill"));
// label = new JLabel();
//
// }
//
// @Override
// public Component getListCellRendererComponent(JList list, Object value,
// int index, boolean isSelected, boolean cellHasFocus) {
// SimulationExtensionConfiguration config = (SimulationExtensionConfiguration) value;
//
//
//
// String s = value.toString();
// setText(s);
//
// // Attempt instantiating, catch any exceptions
// Exception ex = null;
// try {
// Class<?> c = Class.forName(s);
// @SuppressWarnings("unused")
// SimulationListener l = (SimulationListener) c.newInstance();
// } catch (Exception e) {
// ex = e;
// }
//
// if (ex == null) {
// setIcon(Icons.SIMULATION_LISTENER_OK);
// //// Listener instantiated successfully.
// setToolTipText("Listener instantiated successfully.");
// } else {
// setIcon(Icons.SIMULATION_LISTENER_ERROR);
// //// <html>Unable to instantiate listener due to exception:<br>
// setToolTipText("<html>Unable to instantiate listener due to exception:<br>" +
// ex.toString());
// }
//
// if (isSelected) {
// setBackground(list.getSelectionBackground());
// setForeground(list.getSelectionForeground());
// } else {
// setBackground(list.getBackground());
// setForeground(list.getForeground());
// }
// setOpaque(true);
// return this;
// }
// }
} }

View File

@ -71,9 +71,6 @@ import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.TreeModel; import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeSelectionModel; import javax.swing.tree.TreeSelectionModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.sf.openrocket.gui.Resettable; import net.sf.openrocket.gui.Resettable;
import net.sf.openrocket.logging.Markers; import net.sf.openrocket.logging.Markers;
import net.sf.openrocket.startup.Application; import net.sf.openrocket.startup.Application;
@ -81,6 +78,9 @@ import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.Invalidatable; import net.sf.openrocket.util.Invalidatable;
import net.sf.openrocket.util.MemoryManagement; import net.sf.openrocket.util.MemoryManagement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class GUIUtil { public class GUIUtil {
private static final Logger log = LoggerFactory.getLogger(GUIUtil.class); private static final Logger log = LoggerFactory.getLogger(GUIUtil.class);
@ -147,6 +147,7 @@ public class GUIUtil {
installEscapeCloseOperation(dialog); installEscapeCloseOperation(dialog);
setWindowIcons(dialog); setWindowIcons(dialog);
addModelNullingListener(dialog); addModelNullingListener(dialog);
dialog.setLocationRelativeTo(dialog.getOwner());
dialog.setLocationByPlatform(true); dialog.setLocationByPlatform(true);
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
dialog.pack(); dialog.pack();

View File

@ -8,13 +8,13 @@ import java.util.Map;
import javax.swing.Icon; import javax.swing.Icon;
import javax.swing.ImageIcon; import javax.swing.ImageIcon;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.sf.openrocket.document.Simulation; import net.sf.openrocket.document.Simulation;
import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.startup.Application; import net.sf.openrocket.startup.Application;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Icons { public class Icons {
private static final Logger log = LoggerFactory.getLogger(Icons.class); private static final Logger log = LoggerFactory.getLogger(Icons.class);
@ -77,6 +77,8 @@ public class Icons {
public static final Icon DELETE = loadImageIcon("pix/icons/delete.png", "Delete"); public static final Icon DELETE = loadImageIcon("pix/icons/delete.png", "Delete");
public static final Icon EDIT = loadImageIcon("pix/icons/pencil.png", "Edit"); public static final Icon EDIT = loadImageIcon("pix/icons/pencil.png", "Edit");
public static final Icon CONFIGURE = loadImageIcon("pix/icons/configure.png", "Configure");
public static final Icon HELP = loadImageIcon("pix/icons/help-about.png", "Help");
public static final Icon UP = loadImageIcon("pix/icons/up.png", "Up"); public static final Icon UP = loadImageIcon("pix/icons/up.png", "Up");
public static final Icon DOWN = loadImageIcon("pix/icons/down.png", "Down"); public static final Icon DOWN = loadImageIcon("pix/icons/down.png", "Down");

View File

@ -0,0 +1,69 @@
package net.sf.openrocket.simulation.extension;
import java.awt.Dialog.ModalityType;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JPanel;
import net.miginfocom.swing.MigLayout;
import net.sf.openrocket.document.Simulation;
import net.sf.openrocket.gui.util.GUIUtil;
import net.sf.openrocket.l10n.Translator;
import com.google.inject.Inject;
public abstract class AbstractSwingSimulationExtensionConfigurator<E extends SimulationExtension> implements SwingSimulationExtensionConfigurator {
@Inject
protected Translator trans;
private final Class<E> extensionClass;
protected AbstractSwingSimulationExtensionConfigurator(Class<E> extensionClass) {
this.extensionClass = extensionClass;
}
@Override
public boolean support(SimulationExtension extension) {
return extensionClass.isInstance(extension);
}
@SuppressWarnings("unchecked")
@Override
public void configure(SimulationExtension extension, Simulation simulation, Window parent) {
final JDialog dialog = new JDialog(parent, getTitle(extension, simulation), ModalityType.APPLICATION_MODAL);
JPanel panel = new JPanel(new MigLayout("fill"));
JPanel sub = new JPanel(new MigLayout("fill, ins 0"));
panel.add(getConfigurationComponent((E) extension, simulation, sub), "grow, wrap para");
JButton close = new JButton(trans.get("button.close"));
close.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
dialog.setVisible(false);
}
});
panel.add(close, "right");
dialog.add(panel);
GUIUtil.setDisposableDialogOptions(dialog, close);
dialog.setVisible(true);
}
/**
* Return a title for the dialog window. By default uses the extension's name.
*/
protected String getTitle(SimulationExtension extension, Simulation simulation) {
return extension.getName();
}
protected abstract JComponent getConfigurationComponent(E extension, Simulation simulation, JPanel panel);
}

View File

@ -0,0 +1,29 @@
package net.sf.openrocket.simulation.extension;
import java.awt.Window;
import net.sf.openrocket.document.Simulation;
import net.sf.openrocket.plugin.Plugin;
@Plugin
public interface SwingSimulationExtensionConfigurator {
/**
* Test whether this configurator supports configuring an extension.
*
* @param extension the extension to test
* @return true if this configurator can configure the specified extension
*/
public boolean support(SimulationExtension extension);
/**
* Open an application-modal dialog for configuring a simulation extension.
* Close the dialog when ready.
*
* @param extension the extension to configure
* @param simulation the simulation the extension is attached to
* @param parent the parent window for the dialog
*/
public void configure(SimulationExtension extension, Simulation simulation, Window parent);
}

View File

@ -0,0 +1,43 @@
package net.sf.openrocket.simulation.extension.impl;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import net.sf.openrocket.document.Simulation;
import net.sf.openrocket.gui.SpinnerEditor;
import net.sf.openrocket.gui.adaptors.DoubleModel;
import net.sf.openrocket.gui.components.BasicSlider;
import net.sf.openrocket.gui.components.UnitSelector;
import net.sf.openrocket.plugin.Plugin;
import net.sf.openrocket.simulation.extension.AbstractSwingSimulationExtensionConfigurator;
import net.sf.openrocket.unit.UnitGroup;
@Plugin
public class AirStartConfigurator extends AbstractSwingSimulationExtensionConfigurator<AirStart> {
public AirStartConfigurator() {
super(AirStart.class);
}
@Override
protected JComponent getConfigurationComponent(AirStart extension, Simulation simulation, JPanel panel) {
panel.add(new JLabel("Launch altitude:"));
DoubleModel m = new DoubleModel(extension, "LaunchAltitude", UnitGroup.UNITS_DISTANCE, 0);
JSpinner spin = new JSpinner(m.getSpinnerModel());
spin.setEditor(new SpinnerEditor(spin));
panel.add(spin, "w 65lp!");
UnitSelector unit = new UnitSelector(m);
panel.add(unit, "w 25");
BasicSlider slider = new BasicSlider(m.getSliderModel(0, 1000));
panel.add(slider, "w 75lp, wrap");
return panel;
}
}

View File

@ -0,0 +1,47 @@
package net.sf.openrocket.simulation.extension.impl;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import net.sf.openrocket.document.Simulation;
import net.sf.openrocket.plugin.Plugin;
import net.sf.openrocket.simulation.extension.AbstractSwingSimulationExtensionConfigurator;
@Plugin
public class JavaCodeConfigurator extends AbstractSwingSimulationExtensionConfigurator<JavaCode> {
public JavaCodeConfigurator() {
super(JavaCode.class);
}
@Override
protected JComponent getConfigurationComponent(final JavaCode extension, Simulation simulation, JPanel panel) {
panel.add(new JLabel(trans.get("SimulationExtension.javacode.desc")), "wrap para");
panel.add(new JLabel(trans.get("SimulationExtension.javacode.className")), "wrap rel");
final JTextField textField = new JTextField(extension.getClassName());
textField.getDocument().addDocumentListener(new DocumentListener() {
public void changedUpdate(DocumentEvent e) {
update();
}
public void removeUpdate(DocumentEvent e) {
update();
}
public void insertUpdate(DocumentEvent e) {
update();
}
public void update() {
extension.setClassName(textField.getText());
}
});
panel.add(textField, "growx");
return panel;
}
}