diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index be5724c5d..ad0d9e245 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -377,11 +377,9 @@ simedtdlg.lbl.Timestep = Time step: simedtdlg.lbl.ttip.Timestep1 = The time between simulation steps.
A smaller time step results in a more accurate but slower simulation.
simedtdlg.lbl.ttip.Timestep2 = The 4th order simulation method is quite accurate with a time step of simedtdlg.but.ttip.resettodefault = Reset the time step to its default value ( -simedtdlg.border.Simlist = Simulator listeners -simedtdlg.txt.longA1 = Simulation listeners is an advanced feature that allows user-written code to listen to and interact with the simulation. -simedtdlg.txt.longA2 = For details on writing simulation listeners, see the OpenRocket technical documentation. -simedtdlg.lbl.Curlist = Current listeners: -simedtdlg.lbl.Addsimlist = Add simulation listener +simedtdlg.border.SimExt = Simulation extensions +simedtdlg.SimExt.desc = Simulation extensions enable advanced features and custom functionality during flight simulations. You can for example do hardware-in-the-loop testing with them. +simedtdlg.SimExt.noExtensions = No simulation extensions defined simedtdlg.lbl.Noflightdata = No flight data available. simedtdlg.lbl.runsimfirst = Please run the simulation first. simedtdlg.chart.Simflight = Simulated flight @@ -394,6 +392,12 @@ simedtdlg.IntensityDesc.High = High simedtdlg.IntensityDesc.Veryhigh = Very high 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.export = Export SimulationEditDialog.btn.edit = Edit diff --git a/core/resources/pix/icons/configure.png b/core/resources/pix/icons/configure.png new file mode 100644 index 000000000..a4a3834ab Binary files /dev/null and b/core/resources/pix/icons/configure.png differ diff --git a/core/src/net/sf/openrocket/document/OpenRocketDocument.java b/core/src/net/sf/openrocket/document/OpenRocketDocument.java index c479005b9..aec3d0333 100644 --- a/core/src/net/sf/openrocket/document/OpenRocketDocument.java +++ b/core/src/net/sf/openrocket/document/OpenRocketDocument.java @@ -23,7 +23,7 @@ import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.simulation.FlightDataType; 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.util.ArrayList; @@ -146,17 +146,8 @@ public class OpenRocketDocument implements ComponentChangeListener { // simulation listeners for (Simulation sim : simulations) { - for (String className : sim.getSimulationListeners()) { - SimulationListener l = null; - 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); - } + for (SimulationExtension c : sim.getSimulationExtensions()) { + allTypes.addAll(c.getFlightDataTypes()); } } diff --git a/core/src/net/sf/openrocket/document/Simulation.java b/core/src/net/sf/openrocket/document/Simulation.java index 594734e2b..0b672032e 100644 --- a/core/src/net/sf/openrocket/document/Simulation.java +++ b/core/src/net/sf/openrocket/document/Simulation.java @@ -21,7 +21,7 @@ import net.sf.openrocket.simulation.SimulationEngine; import net.sf.openrocket.simulation.SimulationOptions; import net.sf.openrocket.simulation.SimulationStepper; 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.startup.Application; import net.sf.openrocket.util.ArrayList; @@ -76,7 +76,8 @@ public class Simulation implements ChangeSource, Cloneable { // TODO: HIGH: Change to use actual conditions class?? private SimulationOptions options; - private ArrayList simulationListeners = new ArrayList(); + private ArrayList simulationExtensions = new ArrayList(); + private final Class simulationEngineClass = BasicEventSimulationEngine.class; private Class simulationStepperClass = RK4SimulationStepper.class; @@ -116,7 +117,7 @@ public class Simulation implements ChangeSource, Cloneable { public Simulation(Rocket rocket, Status status, String name, SimulationOptions options, - List listeners, FlightData data) { + List extensions, FlightData data) { if (rocket == null) throw new IllegalArgumentException("rocket cannot be null"); @@ -142,8 +143,8 @@ public class Simulation implements ChangeSource, Cloneable { this.options = options; options.addChangeListener(new ConditionListener()); - if (listeners != null) { - this.simulationListeners.addAll(listeners); + if (extensions != null) { + 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. * - * @return the actual list of simulation listeners. + * @return the actual list of simulation extensions. */ - public List getSimulationListeners() { + public List getSimulationExtensions() { mutex.verify(); - return simulationListeners; + return simulationExtensions; } @@ -293,16 +294,8 @@ public class Simulation implements ChangeSource, Cloneable { simulationConditions.getSimulationListenerList().add(l); } - for (String className : simulationListeners) { - SimulationListener l = null; - 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); + for (SimulationExtension extension : simulationExtensions) { + extension.initialize(simulationConditions); } long t1, t2; @@ -410,7 +403,9 @@ public class Simulation implements ChangeSource, Cloneable { copy.mutex = SafetyMutex.newInstance(); copy.status = Status.NOT_SIMULATED; copy.options = this.options.clone(); - copy.simulationListeners = this.simulationListeners.clone(); + for (SimulationExtension c : this.simulationExtensions) { + copy.simulationExtensions.add(c.clone()); + } copy.listeners = new ArrayList(); copy.simulatedConditions = null; copy.simulatedConfiguration = null; @@ -442,7 +437,9 @@ public class Simulation implements ChangeSource, Cloneable { copy.name = this.name; copy.options.copyFrom(this.options); copy.simulatedConfiguration = this.simulatedConfiguration; - copy.simulationListeners = this.simulationListeners.clone(); + for (SimulationExtension c : this.simulationExtensions) { + copy.simulationExtensions.add(c.clone()); + } copy.simulationStepperClass = this.simulationStepperClass; copy.aerodynamicCalculatorClass = this.aerodynamicCalculatorClass; diff --git a/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java b/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java index a3f085331..108c634fa 100644 --- a/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java +++ b/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java @@ -30,8 +30,10 @@ import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.FlightEvent; import net.sf.openrocket.simulation.SimulationOptions; 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.BuildProperties; +import net.sf.openrocket.util.Config; import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.Reflection; import net.sf.openrocket.util.TextUtil; @@ -455,9 +457,18 @@ public class OpenRocketSaver extends RocketSaver { indent--; writeln(""); - - for (String s : simulation.getSimulationListeners()) { - writeElement("listener", TextUtil.escapeXML(s)); + for (SimulationExtension extension : simulation.getSimulationExtensions()) { + Config config = extension.getConfig(); + writeln(""); + indent++; + if (config != null) { + for (String key : config.keySet()) { + Object value = config.get(key, null); + writeEntry(key, value); + } + } + indent--; + writeln(""); } // 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("" + value + ""); + } else if (value instanceof Number) { + writeln("" + value + ""); + } else if (value instanceof String) { + writeln("" + value + ""); + } else if (value instanceof List) { + List list = (List) value; + writeln(""); + indent++; + for (Object o : list) { + writeEntry(null, o); + } + indent--; + writeln(""); + } else { + // Unknown type + log.error("Unknown configuration value type " + value.getClass() + " value=" + value); + } + } + private void saveFlightDataBranch(FlightDataBranch branch, double timeSkip) throws IOException { double previousTime = -100000; diff --git a/core/src/net/sf/openrocket/file/openrocket/importt/ConfigHandler.java b/core/src/net/sf/openrocket/file/openrocket/importt/ConfigHandler.java new file mode 100644 index 000000000..3ecc6cc35 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/importt/ConfigHandler.java @@ -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 list = new ArrayList(); + + @Override + public ElementHandler openElement(String element, HashMap 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 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); + } + } + } +} diff --git a/core/src/net/sf/openrocket/file/openrocket/importt/SingleSimulationHandler.java b/core/src/net/sf/openrocket/file/openrocket/importt/SingleSimulationHandler.java index e0e09e24a..a1d402ffb 100644 --- a/core/src/net/sf/openrocket/file/openrocket/importt/SingleSimulationHandler.java +++ b/core/src/net/sf/openrocket/file/openrocket/importt/SingleSimulationHandler.java @@ -3,6 +3,7 @@ package net.sf.openrocket.file.openrocket.importt; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Set; import net.sf.openrocket.aerodynamics.WarningSet; 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.simulation.FlightData; 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 { @@ -24,9 +32,10 @@ class SingleSimulationHandler extends AbstractElementHandler { private String name; private SimulationConditionsHandler conditionHandler; + private ConfigHandler configHandler; private FlightDataHandler dataHandler; - private final List listeners = new ArrayList(); + private final List extensions = new ArrayList(); public SingleSimulationHandler(OpenRocketDocument doc, DocumentLoadingContext context) { this.doc = doc; @@ -47,6 +56,9 @@ class SingleSimulationHandler extends AbstractElementHandler { } else if (element.equals("conditions")) { conditionHandler = new SimulationConditionsHandler(doc.getRocket(), context); return conditionHandler; + } else if (element.equals("extension")) { + configHandler = new ConfigHandler(); + return configHandler; } else if (element.equals("flightdata")) { dataHandler = new FlightDataHandler(this, context); return dataHandler; @@ -71,7 +83,23 @@ class SingleSimulationHandler extends AbstractElementHandler { warnings.add("Unknown calculator '" + content.trim() + "' specified, ignoring."); } } 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 extensionProviders = Application.getInjector().getInstance(new Key>() { + }); + 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(); Simulation simulation = new Simulation(doc.getRocket(), status, name, - conditions, listeners, data); + conditions, extensions, data); doc.addSimulation(simulation); } + + + private SimulationExtension compatibilityExtension(String className) { + JavaCode extension = Application.getInjector().getInstance(JavaCode.class); + extension.setClassName(className); + return extension; + } + } \ No newline at end of file diff --git a/core/src/net/sf/openrocket/plugin/PluginModule.java b/core/src/net/sf/openrocket/plugin/PluginModule.java index c882f5cba..5985982a0 100644 --- a/core/src/net/sf/openrocket/plugin/PluginModule.java +++ b/core/src/net/sf/openrocket/plugin/PluginModule.java @@ -42,9 +42,8 @@ public class PluginModule extends AbstractModule { if (c.isInterface()) continue; - for (Class intf : c.getInterfaces()) { - - if (interfaces.contains(intf)) { + for (Class intf : interfaces) { + if (intf.isAssignableFrom(c)) { // Ugly hack to enable dynamic binding... Can this be done type-safely? Multibinder binder = (Multibinder) findBinder(intf); binder.addBinding().to(c); diff --git a/core/src/net/sf/openrocket/simulation/extension/AbstractSimulationExtension.java b/core/src/net/sf/openrocket/simulation/extension/AbstractSimulationExtension.java new file mode 100644 index 000000000..8261cee1d --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/AbstractSimulationExtension.java @@ -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 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(); + } +} diff --git a/core/src/net/sf/openrocket/simulation/extension/AbstractSimulationExtensionProvider.java b/core/src/net/sf/openrocket/simulation/extension/AbstractSimulationExtensionProvider.java new file mode 100644 index 000000000..66713805c --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/AbstractSimulationExtensionProvider.java @@ -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 extensionClass; + private final String[] name; + + /** + * Sole constructor. + * + * @param extensionClass the simulation extension class + * @param name the name returned by getName + */ + protected AbstractSimulationExtensionProvider(Class extensionClass, String... name) { + this.extensionClass = extensionClass; + this.name = name; + } + + /** + * By default returns the canonical name of the simulation extension class. + */ + @Override + public List getIds() { + return Arrays.asList(extensionClass.getCanonicalName()); + } + + /** + * By default returns the provided extension name for the first ID that getIds returns. + */ + @Override + public List 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); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/extension/SimulationExtension.java b/core/src/net/sf/openrocket/simulation/extension/SimulationExtension.java new file mode 100644 index 000000000..26fd6a137 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/SimulationExtension.java @@ -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 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); + +} diff --git a/core/src/net/sf/openrocket/simulation/extension/SimulationExtensionConfiguration.java b/core/src/net/sf/openrocket/simulation/extension/SimulationExtensionConfiguration.java new file mode 100644 index 000000000..f588c3219 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/SimulationExtensionConfiguration.java @@ -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 { + + 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 list = new ArrayList(); + 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; + } + + + +} diff --git a/core/src/net/sf/openrocket/simulation/extension/SimulationExtensionProvider.java b/core/src/net/sf/openrocket/simulation/extension/SimulationExtensionProvider.java new file mode 100644 index 000000000..02eb7a2cf --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/SimulationExtensionProvider.java @@ -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 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 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); + +} diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/AirStart.java b/core/src/net/sf/openrocket/simulation/extension/impl/AirStart.java new file mode 100644 index 000000000..4b026e199 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/AirStart.java @@ -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())); + } + } +} diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/AirStartProvider.java b/core/src/net/sf/openrocket/simulation/extension/impl/AirStartProvider.java new file mode 100644 index 000000000..f04458f35 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/AirStartProvider.java @@ -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"); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/JavaCode.java b/core/src/net/sf/openrocket/simulation/extension/impl/JavaCode.java new file mode 100644 index 000000000..8db06c933 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/JavaCode.java @@ -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 list = new ArrayList(); + 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(); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/JavaCodeProvider.java b/core/src/net/sf/openrocket/simulation/extension/impl/JavaCodeProvider.java new file mode 100644 index 000000000..188fe1cc1 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/JavaCodeProvider.java @@ -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"); + } + +} diff --git a/core/src/net/sf/openrocket/util/Config.java b/core/src/net/sf/openrocket/util/Config.java new file mode 100644 index 000000000..64a87e833 --- /dev/null +++ b/core/src/net/sf/openrocket/util/Config.java @@ -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 map = new LinkedHashMap(); + + + 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 keySet() { + return Collections.unmodifiableMap(map).keySet(); + } + + @Override + public Config clone() { + Config copy = new Config(); + for (Entry entry : map.entrySet()) { + copy.map.put(entry.getKey(), clone(entry.getValue())); + } + return copy; + } + + @SuppressWarnings("unchecked") + private T get(String key, T def, Class 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 copy = new ArrayList(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")); + } + } + +} diff --git a/core/test/net/sf/openrocket/util/TestConfig.java b/core/test/net/sf/openrocket/util/TestConfig.java new file mode 100644 index 000000000..5c10ca7a6 --- /dev/null +++ b/core/test/net/sf/openrocket/util/TestConfig.java @@ -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 list = new ArrayList(); + 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 list = new ArrayList(); + 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 list = new ArrayList(); + 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 list = new ArrayList(); + list.add("Foo"); + list.add(new Date()); + try { + config.put("foo", list); + fail(); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void testStoringListWithNull() { + List list = new ArrayList(); + list.add("Foo"); + list.add(new Date()); + try { + config.put("foo", list); + fail(); + } catch (IllegalArgumentException e) { + } + } +} diff --git a/swing/.classpath b/swing/.classpath index db78974b7..c20dd6265 100644 --- a/swing/.classpath +++ b/swing/.classpath @@ -1,25 +1,25 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/swing/.settings/org.eclipse.jdt.core.prefs b/swing/.settings/org.eclipse.jdt.core.prefs index 779e3273d..16438c33d 100644 --- a/swing/.settings/org.eclipse.jdt.core.prefs +++ b/swing/.settings/org.eclipse.jdt.core.prefs @@ -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.nullable=org.eclipse.jdt.annotation.Nullable 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.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.autoboxing=ignore org.eclipse.jdt.core.compiler.problem.comparingIdentical=error 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.discouragedReference=warning 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.fallthroughCase=error 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.missingOverrideAnnotation=ignore 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.noEffectAssignment=error 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.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/swing/src/net/sf/openrocket/gui/simulation/SimulationEditDialog.java b/swing/src/net/sf/openrocket/gui/simulation/SimulationEditDialog.java index 53468bb61..af0ae5298 100644 --- a/swing/src/net/sf/openrocket/gui/simulation/SimulationEditDialog.java +++ b/swing/src/net/sf/openrocket/gui/simulation/SimulationEditDialog.java @@ -26,6 +26,7 @@ import net.sf.openrocket.gui.util.GUIUtil; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.rocketcomponent.Configuration; import net.sf.openrocket.simulation.SimulationOptions; +import net.sf.openrocket.simulation.extension.SimulationExtension; import net.sf.openrocket.startup.Application; @@ -91,8 +92,10 @@ public class SimulationEditDialog extends JDialog { if (simulation.length > 1) { for (int i = 1; i < simulation.length; i++) { simulation[i].getOptions().copyConditionsFrom(simulation[0].getOptions()); - simulation[i].getSimulationListeners().clear(); - simulation[i].getSimulationListeners().addAll(simulation[0].getSimulationListeners()); + simulation[i].getSimulationExtensions().clear(); + for (SimulationExtension c : simulation[0].getSimulationExtensions()) { + simulation[i].getSimulationExtensions().add(c.clone()); + } } } } diff --git a/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java b/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java index c7b0fe023..d91776808 100644 --- a/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java +++ b/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java @@ -1,21 +1,26 @@ 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.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.JButton; import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JDialog; import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JOptionPane; +import javax.swing.JMenu; +import javax.swing.JMenuItem; import javax.swing.JPanel; +import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JSpinner; -import javax.swing.ListCellRenderer; +import javax.swing.MenuElement; import javax.swing.SwingUtilities; 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.components.BasicSlider; 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.util.GUIUtil; import net.sf.openrocket.gui.util.Icons; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.simulation.RK4SimulationStepper; import net.sf.openrocket.simulation.SimulationOptions; -import net.sf.openrocket.simulation.listeners.SimulationListener; -import net.sf.openrocket.simulation.listeners.example.CSVSaveListener; +import net.sf.openrocket.simulation.extension.SimulationExtension; +import net.sf.openrocket.simulation.extension.SimulationExtensionProvider; +import net.sf.openrocket.simulation.extension.SwingSimulationExtensionConfigurator; import net.sf.openrocket.startup.Application; import net.sf.openrocket.unit.UnitGroup; import net.sf.openrocket.util.GeodeticComputationStrategy; +import com.google.inject.Key; + class SimulationOptionsPanel extends JPanel { private static final Translator trans = Application.getTranslator(); final Simulation simulation; + private JPanel currentExtensions; + SimulationOptionsPanel(final Simulation simulation) { super(new MigLayout("fill")); this.simulation = simulation; @@ -162,133 +175,255 @@ class SimulationOptionsPanel extends JPanel { - //// Simulation listeners + //// Simulation extensions sub = new JPanel(new MigLayout("fill, gap 0 0")); - //// Simulator listeners - sub.setBorder(BorderFactory.createTitledBorder(trans.get("simedtdlg.border.Simlist"))); - this.add(sub, "growx, growy"); + sub.setBorder(BorderFactory.createTitledBorder(trans.get("simedtdlg.border.SimExt"))); + this.add(sub, "wmin 300lp, growx, growy"); DescriptionArea desc = new DescriptionArea(5); - //// Simulation listeners is an advanced feature that allows user-written code to listen to and interact with the simulation. - //// For details on writing simulation listeners, see the OpenRocket technical documentation. - desc.setText(trans.get("simedtdlg.txt.longA1") + - trans.get("simedtdlg.txt.longA2")); - sub.add(desc, "aligny 0, growx, wrap para"); + desc.setText(trans.get("simedtdlg.SimExt.desc")); + sub.add(desc, "aligny 0, hmin 100lp, 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 JList list = new JList(listenerModel); - list.setCellRenderer(new ListenerCellRenderer()); - JScrollPane scroll = new JScrollPane(list); - // scroll.setPreferredSize(new Dimension(1,1)); - 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:", - "" + CSVSaveListener.class.getName() + "" }, - //// 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(); + final JButton addExtension = new JButton("Add extension"); + final JPopupMenu menu = getExtensionMenu(); + addExtension.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent ev) { + menu.show(addExtension, 5, addExtension.getBounds().height); } }); - sub.add(button, "split 2, sizegroup buttons, alignx 50%, gapright para"); + sub.add(addExtension, "growx, wrap 0"); - //// Remove button - button = new JButton(trans.get("simedtdlg.but.remove")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - int[] selected = list.getSelectedIndices(); - Arrays.sort(selected); - for (int i = selected.length - 1; i >= 0; i--) { - simulation.getSimulationListeners().remove(selected[i]); + currentExtensions = new JPanel(new MigLayout("fillx, gap 0 0, ins 0")); + JScrollPane scroll = new JScrollPane(currentExtensions); + // &#$%! scroll pane will not honor "growy"... + sub.add(scroll, "growx, growy, h 100%"); + + updateCurrentExtensions(); + + } + + private JPopupMenu getExtensionMenu() { + Set extensions = Application.getInjector().getInstance(new Key>() { + }); + + JPopupMenu basemenu = new JPopupMenu(); + + for (final SimulationExtensionProvider provider : extensions) { + List ids = provider.getIds(); + for (final String id : ids) { + List 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 menuItems) { + for (int i = 0; i < menuItems.size() - 1; i++) { + String menuItem = menuItems.get(i); + + MenuElement found = null; + for (MenuElement e : menu.getSubElements()) { + if (e instanceof JMenu && ((JMenu) e).getText().equals(menuItem)) { + found = e; + break; + } + } + + if (found != null) { + menu = found; + } else { + JMenu m = new JMenu(menuItem); + ((JComponent) menu).add(m); + menu = m; + } + } + return (JComponent) menu; + } + + + private void updateCurrentExtensions() { + currentExtensions.removeAll(); - @Override - public Component getListCellRendererComponent(JList list, Object value, - int index, boolean isSelected, boolean cellHasFocus) { - String s = value.toString(); - setText(s); + 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")); - // 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; + 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 (ex == null) { - setIcon(Icons.SIMULATION_LISTENER_OK); - //// Listener instantiated successfully. - setToolTipText("Listener instantiated successfully."); - } else { - setIcon(Icons.SIMULATION_LISTENER_ERROR); - //// Unable to instantiate listener due to exception:
- setToolTipText("Unable to instantiate listener due to exception:
" + - ex.toString()); + if (extension.getDescription() != null) { + button = new JButton(Icons.HELP); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + 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"); } - if (isSelected) { - setBackground(list.getSelectionBackground()); - setForeground(list.getSelectionForeground()); - } else { - setBackground(list.getBackground()); - setForeground(list.getForeground()); + button = new JButton(Icons.DELETE); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + Iterator 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 configurators = Application.getInjector().getInstance(new Key>() { + }); + for (SwingSimulationExtensionConfigurator c : configurators) { + if (c.support(extension)) { + return c; + } } - setOpaque(true); - return this; + return null; } } - private class ListenerListModel extends AbstractListModel { - @Override - public String getElementAt(int index) { - if (index < 0 || index >= getSize()) - return null; - return simulation.getSimulationListeners().get(index); - } - - @Override - public int getSize() { - return simulation.getSimulationListeners().size(); - } - - public void fireContentsChanged() { - super.fireContentsChanged(this, 0, getSize()); - } - } + // + // + // private class ExtensionListModel extends AbstractListModel { + // @Override + // public SimulationExtensionConfiguration getElementAt(int index) { + // if (index < 0 || index >= getSize()) + // return null; + // return simulation.getSimulationExtensions().get(index); + // } + // + // @Override + // public int getSize() { + // return simulation.getSimulationExtensions().size(); + // } + // } + // + // + // 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); + // //// Unable to instantiate listener due to exception:
+ // setToolTipText("Unable to instantiate listener due to exception:
" + + // ex.toString()); + // } + // + // if (isSelected) { + // setBackground(list.getSelectionBackground()); + // setForeground(list.getSelectionForeground()); + // } else { + // setBackground(list.getBackground()); + // setForeground(list.getForeground()); + // } + // setOpaque(true); + // return this; + // } + // } } diff --git a/swing/src/net/sf/openrocket/gui/util/GUIUtil.java b/swing/src/net/sf/openrocket/gui/util/GUIUtil.java index d11a2daf0..80a1e0fac 100644 --- a/swing/src/net/sf/openrocket/gui/util/GUIUtil.java +++ b/swing/src/net/sf/openrocket/gui/util/GUIUtil.java @@ -71,9 +71,6 @@ import javax.swing.tree.DefaultTreeSelectionModel; import javax.swing.tree.TreeModel; import javax.swing.tree.TreeSelectionModel; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import net.sf.openrocket.gui.Resettable; import net.sf.openrocket.logging.Markers; 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.MemoryManagement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class GUIUtil { private static final Logger log = LoggerFactory.getLogger(GUIUtil.class); @@ -147,6 +147,7 @@ public class GUIUtil { installEscapeCloseOperation(dialog); setWindowIcons(dialog); addModelNullingListener(dialog); + dialog.setLocationRelativeTo(dialog.getOwner()); dialog.setLocationByPlatform(true); dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); dialog.pack(); diff --git a/swing/src/net/sf/openrocket/gui/util/Icons.java b/swing/src/net/sf/openrocket/gui/util/Icons.java index a16836808..402dab84c 100644 --- a/swing/src/net/sf/openrocket/gui/util/Icons.java +++ b/swing/src/net/sf/openrocket/gui/util/Icons.java @@ -8,13 +8,13 @@ import java.util.Map; import javax.swing.Icon; import javax.swing.ImageIcon; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import net.sf.openrocket.document.Simulation; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.startup.Application; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class Icons { 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 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 DOWN = loadImageIcon("pix/icons/down.png", "Down"); diff --git a/swing/src/net/sf/openrocket/simulation/extension/AbstractSwingSimulationExtensionConfigurator.java b/swing/src/net/sf/openrocket/simulation/extension/AbstractSwingSimulationExtensionConfigurator.java new file mode 100644 index 000000000..444c993fd --- /dev/null +++ b/swing/src/net/sf/openrocket/simulation/extension/AbstractSwingSimulationExtensionConfigurator.java @@ -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 implements SwingSimulationExtensionConfigurator { + + @Inject + protected Translator trans; + + private final Class extensionClass; + + protected AbstractSwingSimulationExtensionConfigurator(Class 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); + +} diff --git a/swing/src/net/sf/openrocket/simulation/extension/SwingSimulationExtensionConfigurator.java b/swing/src/net/sf/openrocket/simulation/extension/SwingSimulationExtensionConfigurator.java new file mode 100644 index 000000000..bdd37d398 --- /dev/null +++ b/swing/src/net/sf/openrocket/simulation/extension/SwingSimulationExtensionConfigurator.java @@ -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); + +} diff --git a/swing/src/net/sf/openrocket/simulation/extension/impl/AirStartConfigurator.java b/swing/src/net/sf/openrocket/simulation/extension/impl/AirStartConfigurator.java new file mode 100644 index 000000000..65dd6f9b4 --- /dev/null +++ b/swing/src/net/sf/openrocket/simulation/extension/impl/AirStartConfigurator.java @@ -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 { + + 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; + } + +} diff --git a/swing/src/net/sf/openrocket/simulation/extension/impl/JavaCodeConfigurator.java b/swing/src/net/sf/openrocket/simulation/extension/impl/JavaCodeConfigurator.java new file mode 100644 index 000000000..f5bcb453e --- /dev/null +++ b/swing/src/net/sf/openrocket/simulation/extension/impl/JavaCodeConfigurator.java @@ -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 { + + 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; + } + +}