diff --git a/core/fileformat.txt b/core/fileformat.txt index 311c0dbfa..4484bde6f 100644 --- a/core/fileformat.txt +++ b/core/fileformat.txt @@ -47,3 +47,6 @@ The following file format versions exist: 1.6: Introduced with OpenRocket 13.04. Added component Appearances (decals & paint) Added configurable parameters to recovery devices, motor ignition and separation. +1.7: Introduced with OpenRocket 15.01. Added simulation extensions and related + configuration. + \ No newline at end of file diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index 7aa8d4dbd..d05069199 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -402,11 +402,11 @@ 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.SimExt.add = Add extension +simedtdlg.SimExt.copyExtension = Copy extension simedtdlg.lbl.Noflightdata = No flight data available. simedtdlg.lbl.runsimfirst = Please run the simulation first. simedtdlg.chart.Simflight = Simulated flight @@ -419,6 +419,26 @@ simedtdlg.IntensityDesc.High = High simedtdlg.IntensityDesc.Veryhigh = Very high simedtdlg.IntensityDesc.Extreme = Extreme +SimulationExtension.airstart.name.alt = Air-start ({alt}) +SimulationExtension.airstart.name.altvel = Air-start ({alt}, {vel}) +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: + +SimulationExtension.scripting.name = {language} script +SimulationExtension.scripting.desc = Extend OpenRocket simulations by custom scripts. +SimulationExtension.scripting.language.label = Language: +SimulationExtension.scripting.warning.disabled = Untrusted scripts have been disabled. You need to manually enable them in the Simulation options. +SimulationExtension.scripting.text.enabled = Enable script +SimulationExtension.scripting.text.enabled.ttip = The script is run only when enabled. +SimulationExtension.scripting.text.trusted = Trust this script on this computer +SimulationExtension.scripting.text.trusted.msg = Untrusted scripts are disabled when loading the document +SimulationExtension.scripting.text.trusted.clear = Clear trusted scripts +SimulationExtension.scripting.text.trusted.clear.ttip = Clear the trusted status of all scripts on this computer +SimulationExtension.scripting.text.trusted.cleared = All scripts are now untrusted on this computer. +SimulationExtension.scripting.text.trusted.cleared.title = Cleared + 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 da5c74976..5a8b8ed23 100644 --- a/core/src/net/sf/openrocket/document/Simulation.java +++ b/core/src/net/sf/openrocket/document/Simulation.java @@ -28,7 +28,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; @@ -86,7 +86,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 extends SimulationEngine> simulationEngineClass = BasicEventSimulationEngine.class; private Class extends SimulationStepper> simulationStepperClass = RK4SimulationStepper.class; @@ -126,7 +127,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"); @@ -152,8 +153,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); } @@ -206,14 +207,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; } @@ -325,16 +326,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; @@ -442,7 +435,10 @@ 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(); + copy.simulationExtensions = new ArrayList(); + for (SimulationExtension c : this.simulationExtensions) { + copy.simulationExtensions.add(c.clone()); + } copy.listeners = new ArrayList(); copy.simulatedConditions = null; copy.simulatedConfiguration = null; @@ -474,7 +470,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 cea1e2a9d..4fa7c4e57 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; @@ -217,6 +219,11 @@ public class OpenRocketSaver extends RocketSaver { */ private int calculateNecessaryFileVersion(OpenRocketDocument document, StorageOptions opts) { /* + * NOTE: Remember to update the supported versions in DocumentConfig as well! + * + * File version 1.7 is required for: + * - simulation extensions + * * File version 1.6 is required for: * - saving files using appearances and textures, flight configurations. * @@ -236,6 +243,16 @@ public class OpenRocketSaver extends RocketSaver { * Otherwise use version 1.0. */ + ///////////////// + // Version 1.7 // + ///////////////// + for (Simulation sim : document.getSimulations()) { + if (!sim.getSimulationExtensions().isEmpty()) { + return FILE_VERSION_DIVISOR + 7; + } + } + + ///////////////// // Version 1.6 // ///////////////// @@ -455,9 +472,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 +538,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("" + TextUtil.escapeXML((String) 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..ec2f7b8f7 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/importt/ConfigHandler.java @@ -0,0 +1,89 @@ +package net.sf.openrocket.file.openrocket.importt; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.HashMap; +import java.util.List; + +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; + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/importt/DocumentConfig.java b/core/src/net/sf/openrocket/file/openrocket/importt/DocumentConfig.java index a1d207a8a..4734ddffb 100644 --- a/core/src/net/sf/openrocket/file/openrocket/importt/DocumentConfig.java +++ b/core/src/net/sf/openrocket/file/openrocket/importt/DocumentConfig.java @@ -48,7 +48,7 @@ import net.sf.openrocket.util.Reflection; class DocumentConfig { /* Remember to update OpenRocketSaver as well! */ - public static final String[] SUPPORTED_VERSIONS = { "1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6" }; + public static final String[] SUPPORTED_VERSIONS = { "1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6", "1.7" }; /** * Divisor used in converting an integer version to the point-represented version. diff --git a/core/src/net/sf/openrocket/file/openrocket/importt/OpenRocketLoader.java b/core/src/net/sf/openrocket/file/openrocket/importt/OpenRocketLoader.java index 43840ddc8..464dda9c4 100644 --- a/core/src/net/sf/openrocket/file/openrocket/importt/OpenRocketLoader.java +++ b/core/src/net/sf/openrocket/file/openrocket/importt/OpenRocketLoader.java @@ -13,6 +13,7 @@ import net.sf.openrocket.file.RocketLoadException; import net.sf.openrocket.file.simplesax.SimpleSAX; import net.sf.openrocket.simulation.FlightDataBranch; import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.simulation.extension.SimulationExtension; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -77,12 +78,18 @@ public class OpenRocketLoader extends AbstractRocketLoader { previousTime = time; } } - // Round value timeSkip = Math.rint(timeSkip * 100) / 100; - doc.getDefaultStorageOptions().setSimulationTimeSkip(timeSkip); doc.getDefaultStorageOptions().setExplicitlySet(false); + // Call simulation extensions + for (Simulation sim : doc.getSimulations()) { + for (SimulationExtension ext : sim.getSimulationExtensions()) { + ext.documentLoaded(doc, sim, warnings); + } + } + + doc.clearUndo(); log.info("Loading done"); } 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/SimulationConditions.java b/core/src/net/sf/openrocket/simulation/SimulationConditions.java index 067417d3b..c1c5f2257 100644 --- a/core/src/net/sf/openrocket/simulation/SimulationConditions.java +++ b/core/src/net/sf/openrocket/simulation/SimulationConditions.java @@ -12,6 +12,7 @@ import net.sf.openrocket.models.wind.WindModel; import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.simulation.listeners.SimulationListener; import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.GeodeticComputationStrategy; import net.sf.openrocket.util.Monitorable; import net.sf.openrocket.util.WorldCoordinate; @@ -38,11 +39,14 @@ public class SimulationConditions implements Monitorable, Cloneable { /** Launch rod direction, 0 = north */ private double launchRodDirection = 0; - // TODO: Depreciate these and use worldCoordinate only. - //private double launchAltitude = 0; - //private double launchLatitude = 45; - //private double launchLongitude = 0; + // Launch site location (lat, lon, alt) private WorldCoordinate launchSite = new WorldCoordinate(0, 0, 0); + + // Launch location in simulation coordinates (normally always 0, air-start would override this) + private Coordinate launchPosition = Coordinate.NUL; + + private Coordinate launchVelocity = Coordinate.NUL; + private GeodeticComputationStrategy geodeticComputation = GeodeticComputationStrategy.SPHERICAL; @@ -166,6 +170,29 @@ public class SimulationConditions implements Monitorable, Cloneable { } + public Coordinate getLaunchPosition() { + return launchPosition; + } + + public void setLaunchPosition(Coordinate launchPosition) { + if (this.launchPosition.equals(launchPosition)) + return; + this.launchPosition = launchPosition; + this.modID++; + } + + public Coordinate getLaunchVelocity() { + return launchVelocity; + } + + public void setLaunchVelocity(Coordinate launchVelocity) { + if (this.launchVelocity.equals(launchVelocity)) + return; + this.launchVelocity = launchVelocity; + this.modID++; + } + + public GeodeticComputationStrategy getGeodeticComputation() { return geodeticComputation; } diff --git a/core/src/net/sf/openrocket/simulation/SimulationStatus.java b/core/src/net/sf/openrocket/simulation/SimulationStatus.java index 17998a551..bad65b1c9 100644 --- a/core/src/net/sf/openrocket/simulation/SimulationStatus.java +++ b/core/src/net/sf/openrocket/simulation/SimulationStatus.java @@ -95,8 +95,8 @@ public class SimulationStatus implements Monitorable { this.time = 0; this.previousTimeStep = this.simulationConditions.getTimeStep(); - this.position = Coordinate.NUL; - this.velocity = Coordinate.NUL; + this.position = this.simulationConditions.getLaunchPosition(); + this.velocity = this.simulationConditions.getLaunchVelocity(); this.worldPosition = this.simulationConditions.getLaunchSite(); // Initialize to roll angle with least stability w.r.t. the wind 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..fbed23a43 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/AbstractSimulationExtension.java @@ -0,0 +1,122 @@ +package net.sf.openrocket.simulation.extension; + +import java.util.Collections; +import java.util.List; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.Simulation; +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(); + } + } + + /** + * {@inheritDoc} + * + * By default, this method returns the canonical name of this class. + */ + @Override + public String getId() { + return this.getClass().getCanonicalName(); + } + + /** + * {@inheritDoc} + * + * By default, this method returns the name provided to the constructor. + */ + @Override + public String getName() { + return name; + } + + /** + * {@inheritDoc} + * + * By default, this method returns null. + */ + @Override + public String getDescription() { + return null; + } + + /** + * {@inheritDoc} + * + * By default, this method returns an empty list. + */ + @Override + public List getFlightDataTypes() { + return Collections.emptyList(); + } + + /** + * {@inheritDoc} + * + * By default, this method does nothing. + */ + @Override + public void documentLoaded(OpenRocketDocument document, Simulation simulation, WarningSet warnings) { + + } + + /** + * By default, returns a new object obtained by calling Object.clone() and + * cloning the config object. + */ + @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 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 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..f0b6c93c2 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/SimulationExtension.java @@ -0,0 +1,94 @@ +package net.sf.openrocket.simulation.extension; + +import java.util.List; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.Simulation; +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(); + + /** + * Called once for each simulation this extension is attached to when loading a document. + * This may perform necessary changes to the document at load time. + * + * @param document the loaded document + * @param simulation the simulation this extension is attached to + * @param warnings the document loading warnings + */ + public void documentLoaded(OpenRocketDocument document, Simulation simulation, WarningSet warnings); + + /** + * 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..e591685b3 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/AirStart.java @@ -0,0 +1,58 @@ +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; + if (getLaunchVelocity() > 0.01) { + name = trans.get("SimulationExtension.airstart.name.altvel"); + } else { + name = trans.get("SimulationExtension.airstart.name.alt"); + } + name = L10N.replace(name, "{alt}", UnitGroup.UNITS_DISTANCE.toStringUnit(getLaunchAltitude())); + name = L10N.replace(name, "{vel}", UnitGroup.UNITS_VELOCITY.toStringUnit(getLaunchVelocity())); + return name; + } + + public double getLaunchAltitude() { + return config.getDouble("launchAltitude", 0.0); + } + + public void setLaunchAltitude(double launchAltitude) { + config.put("launchAltitude", launchAltitude); + fireChangeEvent(); + } + + public double getLaunchVelocity() { + return config.getDouble("launchVelocity", 0.0); + } + + public void setLaunchVelocity(double launchVelocity) { + config.put("launchVelocity", launchVelocity); + fireChangeEvent(); + } + + + private class AirStartListener extends AbstractSimulationListener { + @Override + public void startSimulation(SimulationStatus status) throws SimulationException { + status.setRocketPosition(new Coordinate(0, 0, getLaunchAltitude())); + status.setRocketVelocity(status.getRocketOrientationQuaternion().rotate(new Coordinate(0, 0, getLaunchVelocity()))); + } + } +} 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/simulation/extension/impl/ScriptingExtension.java b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingExtension.java new file mode 100644 index 000000000..3569d2aae --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingExtension.java @@ -0,0 +1,112 @@ +package net.sf.openrocket.simulation.extension.impl; + +import javax.script.Invocable; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.l10n.L10N; +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 com.google.inject.Inject; + +public class ScriptingExtension extends AbstractSimulationExtension { + + private static final String DEFAULT_LANGUAGE = "JavaScript"; + + @Inject + private ScriptingUtil util; + + + public ScriptingExtension() { + setLanguage(DEFAULT_LANGUAGE); + setScript(""); + setEnabled(true); + } + + @Override + public String getName() { + String name = trans.get("SimulationExtension.scripting.name"); + name = L10N.replace(name, "{language}", getLanguage()); + return name; + } + + @Override + public String getDescription() { + return trans.get("SimulationExtension.scripting.desc"); + } + + @Override + public void documentLoaded(OpenRocketDocument document, Simulation simulation, WarningSet warnings) { + /* + * Scripts that the user has not explicitly indicated as trusted are disabled + * when loading from a file. This is to prevent trojans. + */ + if (isEnabled()) { + if (!util.isTrustedScript(getLanguage(), getScript())) { + setEnabled(false); + warnings.add(Warning.fromString(trans.get("SimulationExtension.scripting.warning.disabled"))); + } + } + } + + @Override + public void initialize(SimulationConditions conditions) throws SimulationException { + if (isEnabled()) { + conditions.getSimulationListenerList().add(getListener()); + } + } + + + public String getScript() { + return config.getString("script", ""); + } + + public void setScript(String script) { + config.put("script", script); + } + + public String getLanguage() { + return config.getString("language", DEFAULT_LANGUAGE); + } + + public void setLanguage(String language) { + config.put("language", language); + } + + public boolean isEnabled() { + return config.getBoolean("enabled", false); + } + + public void setEnabled(boolean enabled) { + config.put("enabled", enabled); + } + + + SimulationListener getListener() throws SimulationException { + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName(getLanguage()); + if (engine == null) { + throw new SimulationException("Your JRE does not support the scripting language '" + getLanguage() + "'"); + } + + try { + engine.eval(getScript()); + } catch (ScriptException e) { + throw new SimulationException("Invalid script: " + e.getMessage()); + } + + if (!(engine instanceof Invocable)) { + throw new SimulationException("The scripting language '" + getLanguage() + "' does not implement the Invocable interface"); + } + return new ScriptingSimulationListener((Invocable) engine); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingProvider.java b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingProvider.java new file mode 100644 index 000000000..911b47214 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingProvider.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 ScriptingProvider extends AbstractSimulationExtensionProvider { + + public ScriptingProvider() { + super(ScriptingExtension.class, "User code", "Scripts"); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingSimulationListener.java b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingSimulationListener.java new file mode 100644 index 000000000..5eab2dfa8 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingSimulationListener.java @@ -0,0 +1,227 @@ +package net.sf.openrocket.simulation.extension.impl; + +import java.util.HashSet; +import java.util.Set; + +import javax.script.Invocable; +import javax.script.ScriptException; + +import net.sf.openrocket.aerodynamics.AerodynamicForces; +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.models.atmosphere.AtmosphericConditions; +import net.sf.openrocket.motor.MotorId; +import net.sf.openrocket.motor.MotorInstance; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.rocketcomponent.RecoveryDevice; +import net.sf.openrocket.simulation.AccelerationData; +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.simulation.MassData; +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.simulation.exception.SimulationListenerException; +import net.sf.openrocket.simulation.listeners.SimulationComputationListener; +import net.sf.openrocket.simulation.listeners.SimulationEventListener; +import net.sf.openrocket.simulation.listeners.SimulationListener; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Coordinate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ScriptingSimulationListener implements SimulationListener, SimulationComputationListener, SimulationEventListener, Cloneable { + + private final static Logger logger = LoggerFactory.getLogger(ScriptingSimulationListener.class); + + /* + * NOTE: This class is used instead of using the scripting interface API + * so that unimplemented script methods are not called unnecessarily. + */ + + private Invocable invocable; + private Set missing = new HashSet(); + + + public ScriptingSimulationListener(Invocable invocable) { + this.invocable = invocable; + } + + + @Override + public boolean isSystemListener() { + return false; + } + + + @Override + public SimulationListener clone() { + try { + ScriptingSimulationListener clone = (ScriptingSimulationListener) super.clone(); + clone.missing = new HashSet(missing); + return clone; + } catch (CloneNotSupportedException e) { + throw new BugException(e); + } + } + + + + //// SimulationListener //// + + @Override + public void startSimulation(SimulationStatus status) throws SimulationException { + invoke(Void.class, null, "startSimulation", status); + } + + @Override + public void endSimulation(SimulationStatus status, SimulationException exception) { + try { + invoke(Void.class, null, "endSimulation", status, exception); + } catch (SimulationException e) { + } + } + + @Override + public boolean preStep(SimulationStatus status) throws SimulationException { + return invoke(Boolean.class, true, "preStep", status); + } + + @Override + public void postStep(SimulationStatus status) throws SimulationException { + invoke(Void.class, null, "postStep", status); + } + + + + //// SimulationEventListener //// + + @Override + public boolean addFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { + return invoke(Boolean.class, true, "addFlightEvent", status, event); + } + + @Override + public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { + return invoke(Boolean.class, true, "handleFlightEvent", status, event); + } + + @Override + public boolean motorIgnition(SimulationStatus status, MotorId motorId, MotorMount mount, MotorInstance instance) throws SimulationException { + return invoke(Boolean.class, true, "motorIgnition", status, motorId, mount, instance); + } + + @Override + public boolean recoveryDeviceDeployment(SimulationStatus status, RecoveryDevice recoveryDevice) throws SimulationException { + return invoke(Boolean.class, true, "recoveryDeviceDeployment", status, recoveryDevice); + } + + + + //// SimulationComputationListener //// + + @Override + public AccelerationData preAccelerationCalculation(SimulationStatus status) throws SimulationException { + return invoke(AccelerationData.class, null, "preAccelerationCalculation", status); + } + + @Override + public AerodynamicForces preAerodynamicCalculation(SimulationStatus status) throws SimulationException { + return invoke(AerodynamicForces.class, null, "preAerodynamicCalculation", status); + } + + @Override + public AtmosphericConditions preAtmosphericModel(SimulationStatus status) throws SimulationException { + return invoke(AtmosphericConditions.class, null, "preAtmosphericModel", status); + } + + @Override + public FlightConditions preFlightConditions(SimulationStatus status) throws SimulationException { + return invoke(FlightConditions.class, null, "preFlightConditions", status); + } + + @Override + public double preGravityModel(SimulationStatus status) throws SimulationException { + return invoke(Double.class, Double.NaN, "preGravityModel", status); + } + + @Override + public MassData preMassCalculation(SimulationStatus status) throws SimulationException { + return invoke(MassData.class, null, "preMassCalculation", status); + } + + @Override + public double preSimpleThrustCalculation(SimulationStatus status) throws SimulationException { + return invoke(Double.class, Double.NaN, "preSimpleThrustCalculation", status); + } + + @Override + public Coordinate preWindModel(SimulationStatus status) throws SimulationException { + return invoke(Coordinate.class, null, "preWindModel", status); + } + + @Override + public AccelerationData postAccelerationCalculation(SimulationStatus status, AccelerationData acceleration) throws SimulationException { + return invoke(AccelerationData.class, null, "postAccelerationCalculation", status, acceleration); + } + + @Override + public AerodynamicForces postAerodynamicCalculation(SimulationStatus status, AerodynamicForces forces) throws SimulationException { + return invoke(AerodynamicForces.class, null, "postAerodynamicCalculation", status, forces); + } + + @Override + public AtmosphericConditions postAtmosphericModel(SimulationStatus status, AtmosphericConditions atmosphericConditions) throws SimulationException { + return invoke(AtmosphericConditions.class, null, "postAtmosphericModel", status, atmosphericConditions); + } + + @Override + public FlightConditions postFlightConditions(SimulationStatus status, FlightConditions flightConditions) throws SimulationException { + return invoke(FlightConditions.class, null, "postFlightConditions", status, flightConditions); + } + + @Override + public double postGravityModel(SimulationStatus status, double gravity) throws SimulationException { + return invoke(Double.class, Double.NaN, "postGravityModel", status, gravity); + } + + @Override + public MassData postMassCalculation(SimulationStatus status, MassData massData) throws SimulationException { + return invoke(MassData.class, null, "postMassCalculation", status, massData); + } + + @Override + public double postSimpleThrustCalculation(SimulationStatus status, double thrust) throws SimulationException { + return invoke(Double.class, Double.NaN, "postSimpleThrustCalculation", status, thrust); + } + + @Override + public Coordinate postWindModel(SimulationStatus status, Coordinate wind) throws SimulationException { + return invoke(Coordinate.class, null, "postWindModel", status, wind); + } + + + @SuppressWarnings("unchecked") + private T invoke(Class retType, T def, String method, Object... args) throws SimulationException { + try { + if (!missing.contains(method)) { + Object o = invocable.invokeFunction(method, args); + if (o == null) { + // Use default/null if function returns nothing + return def; + } else if (!o.getClass().equals(retType)) { + throw new SimulationListenerException("Custom script function " + method + " returned type " + + o.getClass().getSimpleName() + ", expected " + retType.getSimpleName()); + } else { + return (T) o; + } + } + } catch (NoSuchMethodException e) { + missing.add(method); + // fall-through + } catch (ScriptException e) { + logger.warn("Script exception in " + method + ": " + e, e); + throw new SimulationException("Script failed: " + e.getMessage()); + } + return def; + } + +} diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingUtil.java b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingUtil.java new file mode 100644 index 000000000..6f0f76517 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingUtil.java @@ -0,0 +1,158 @@ +package net.sf.openrocket.simulation.extension.impl; + +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.List; +import java.util.prefs.BackingStoreException; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; +import javax.script.ScriptEngineManager; + +import net.sf.openrocket.startup.Preferences; +import net.sf.openrocket.util.ArrayList; +import net.sf.openrocket.util.BugException; + +import com.google.inject.Inject; + +/** + * Utility class used by the scripting extension and its configurator. + */ +public class ScriptingUtil { + + static final String NODE_ID = ScriptingExtension.class.getCanonicalName(); + + private static final List DEFAULT_TRUSTED_HASHES = Arrays.asList( + // Roll control script in roll control example file: + "SHA-256:9bf364ce4d4a75f09b29178bf9d6872b232084f73dae20dc7b5b073e54e95a42" + ); + + /** The name to be chosen from a list of alternatives. If not found, will use the default name. */ + private static final List PREFERRED_LANGUAGE_NAMES = Arrays.asList("JavaScript"); + + @Inject + Preferences prefs; + + + + + /** + * Return the preferred internal language name based on a script language name. + * + * @return the preferred language name, or null if the language is not supported. + */ + public String getLanguage(String language) { + if (language == null) { + return null; + } + + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName(language); + if (engine == null) { + return null; + } + return getLanguage(engine.getFactory()); + } + + + public List getLanguages() { + List langs = new ArrayList(); + ScriptEngineManager manager = new ScriptEngineManager(); + for (ScriptEngineFactory factory : manager.getEngineFactories()) { + langs.add(getLanguage(factory)); + } + return langs; + } + + + private String getLanguage(ScriptEngineFactory factory) { + for (String name : factory.getNames()) { + if (PREFERRED_LANGUAGE_NAMES.contains(name)) { + return name; + } + } + + return factory.getLanguageName(); + } + + + + /** + * Test whether the user has indicated this script to be trusted, + * or if it is an internally trusted script. + */ + public boolean isTrustedScript(String language, String script) { + if (language == null || script == null) { + return false; + } + script = normalize(script); + if (script.length() == 0) { + return true; + } + String hash = hash(language, script); + if (DEFAULT_TRUSTED_HASHES.contains(hash)) { + return true; + } + return prefs.getNode(NODE_ID).getBoolean(hash, false); + } + + /** + * Mark a script as trusted. + */ + public void setTrustedScript(String language, String script, boolean trusted) { + script = normalize(script); + String hash = hash(language, script); + if (trusted) { + prefs.getNode(NODE_ID).putBoolean(hash, true); + } else { + prefs.getNode(NODE_ID).remove(hash); + } + } + + /** + * Clear all trusted scripts. + */ + public void clearTrustedScripts() { + try { + prefs.getNode(NODE_ID).clear(); + } catch (BackingStoreException e) { + throw new BugException(e); + } + } + + + static String normalize(String script) { + return script.replaceAll("\r", "").trim(); + } + + static String hash(String language, String script) { + /* + * NOTE: Hash length must be max 80 chars, the max length of a key in a Properties object. + */ + + String output; + MessageDigest digest; + + try { + digest = MessageDigest.getInstance("SHA-256"); + digest.update(language.getBytes("UTF-8")); + digest.update((byte) '|'); + byte[] hash = digest.digest(script.getBytes("UTF-8")); + BigInteger bigInt = new BigInteger(1, hash); + output = bigInt.toString(16); + while (output.length() < 64) { + output = "0" + output; + } + } catch (NoSuchAlgorithmException e) { + throw new BugException("JRE does not support SHA-256 hash algorithm", e); + } catch (UnsupportedEncodingException e) { + throw new BugException(e); + } + + return digest.getAlgorithm() + ":" + output; + } + +} diff --git a/core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java b/core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java index 0c8a3d32d..1ad3ef4ad 100644 --- a/core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java +++ b/core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java @@ -8,11 +8,11 @@ import net.sf.openrocket.motor.MotorInstance; import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.rocketcomponent.RecoveryDevice; import net.sf.openrocket.simulation.AccelerationData; -import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.FlightEvent; import net.sf.openrocket.simulation.MassData; import net.sf.openrocket.simulation.SimulationStatus; import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.Coordinate; @@ -24,20 +24,10 @@ import net.sf.openrocket.util.Coordinate; * @author Sampo Niskanen */ public class AbstractSimulationListener implements SimulationListener, SimulationComputationListener, - SimulationEventListener { + SimulationEventListener, Cloneable { //// SimulationListener //// - @Override - public String getName() { - return this.getClass().getSimpleName(); - } - - @Override - public String[] getMenuPosition() { - return new String[0]; - } - @Override public void startSimulation(SimulationStatus status) throws SimulationException { // No-op @@ -68,14 +58,6 @@ public class AbstractSimulationListener implements SimulationListener, Simulatio return false; } - /** - * Return an array of any flight data types this listener creates. - */ - @Override - public FlightDataType[] getFlightDataTypes() { - return new FlightDataType[] {}; - } - //// SimulationEventListener //// @@ -184,8 +166,12 @@ public class AbstractSimulationListener implements SimulationListener, Simulatio } @Override - public AbstractSimulationListener clone() throws CloneNotSupportedException { - return (AbstractSimulationListener) super.clone(); + public AbstractSimulationListener clone() { + try { + return (AbstractSimulationListener) super.clone(); + } catch (CloneNotSupportedException e) { + throw new BugException(e); + } } } diff --git a/core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java b/core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java index c02b25765..88422d72c 100644 --- a/core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java +++ b/core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java @@ -4,7 +4,6 @@ import net.sf.openrocket.aerodynamics.AerodynamicForces; import net.sf.openrocket.aerodynamics.FlightConditions; import net.sf.openrocket.models.atmosphere.AtmosphericConditions; import net.sf.openrocket.simulation.AccelerationData; -import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.MassData; import net.sf.openrocket.simulation.SimulationStatus; import net.sf.openrocket.simulation.exception.SimulationException; @@ -17,20 +16,20 @@ import net.sf.openrocket.util.Coordinate; * @author Sampo Niskanen */ public interface SimulationComputationListener extends SimulationListener { - - + + //////// Computation/modeling related callbacks //////// public AccelerationData preAccelerationCalculation(SimulationStatus status) throws SimulationException; public AccelerationData postAccelerationCalculation(SimulationStatus status, AccelerationData acceleration) - throws SimulationException; + throws SimulationException; public AtmosphericConditions preAtmosphericModel(SimulationStatus status) - throws SimulationException; + throws SimulationException; public AtmosphericConditions postAtmosphericModel(SimulationStatus status, AtmosphericConditions atmosphericConditions) - throws SimulationException; + throws SimulationException; public Coordinate preWindModel(SimulationStatus status) throws SimulationException; @@ -42,29 +41,27 @@ public interface SimulationComputationListener extends SimulationListener { public double postGravityModel(SimulationStatus status, double gravity) throws SimulationException; - + public FlightConditions preFlightConditions(SimulationStatus status) - throws SimulationException; + throws SimulationException; public FlightConditions postFlightConditions(SimulationStatus status, FlightConditions flightConditions) - throws SimulationException; + throws SimulationException; public AerodynamicForces preAerodynamicCalculation(SimulationStatus status) - throws SimulationException; + throws SimulationException; public AerodynamicForces postAerodynamicCalculation(SimulationStatus status, AerodynamicForces forces) - throws SimulationException; + throws SimulationException; public MassData preMassCalculation(SimulationStatus status) throws SimulationException; public MassData postMassCalculation(SimulationStatus status, MassData massData) throws SimulationException; - - + + public double preSimpleThrustCalculation(SimulationStatus status) throws SimulationException; public double postSimpleThrustCalculation(SimulationStatus status, double thrust) throws SimulationException; - - @Override - public FlightDataType[] getFlightDataTypes(); + } diff --git a/core/src/net/sf/openrocket/simulation/listeners/SimulationEventListener.java b/core/src/net/sf/openrocket/simulation/listeners/SimulationEventListener.java index 00ef6b7b5..300fb8645 100644 --- a/core/src/net/sf/openrocket/simulation/listeners/SimulationEventListener.java +++ b/core/src/net/sf/openrocket/simulation/listeners/SimulationEventListener.java @@ -4,14 +4,13 @@ import net.sf.openrocket.motor.MotorId; import net.sf.openrocket.motor.MotorInstance; import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.rocketcomponent.RecoveryDevice; -import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.FlightEvent; import net.sf.openrocket.simulation.SimulationStatus; import net.sf.openrocket.simulation.exception.SimulationException; public interface SimulationEventListener { - + /** * Called before adding a flight event to the event queue. * @@ -23,7 +22,7 @@ public interface SimulationEventListener { public boolean addFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException; - + /** * Called before handling a flight event. * @@ -57,10 +56,6 @@ public interface SimulationEventListener { */ public boolean recoveryDeviceDeployment(SimulationStatus status, RecoveryDevice recoveryDevice) throws SimulationException; - - - - public FlightDataType[] getFlightDataTypes(); - + } diff --git a/core/src/net/sf/openrocket/simulation/listeners/SimulationListener.java b/core/src/net/sf/openrocket/simulation/listeners/SimulationListener.java index bdbf90ffc..a0d4b147c 100644 --- a/core/src/net/sf/openrocket/simulation/listeners/SimulationListener.java +++ b/core/src/net/sf/openrocket/simulation/listeners/SimulationListener.java @@ -1,6 +1,5 @@ package net.sf.openrocket.simulation.listeners; -import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.SimulationStatus; import net.sf.openrocket.simulation.exception.SimulationException; @@ -10,26 +9,7 @@ import net.sf.openrocket.simulation.exception.SimulationException; * If the implementation maintains any state, it should be properly cloned. * */ -public interface SimulationListener extends Cloneable { - - /** - * Get the name of this simulation listener. Ideally this should be localized, as - * it can be displayed in the UI. - * - * @return the name of this simulation listener. - */ - public String getName(); - - - /** - * Get the menu position of this simulation listener. This should be an array - * of localized submenu names in descending order, or an empty array for positioning - * in the base menu. - * - * @return the menu position of this simulation listener. - */ - public String[] getMenuPosition(); - +public interface SimulationListener { /** * Called when starting a simulation. @@ -83,9 +63,7 @@ public interface SimulationListener extends Cloneable { /** - * Return a list of any flight data types this listener creates. + * Return a deep copy of this simulation listener including its state. */ - public FlightDataType[] getFlightDataTypes(); - - public SimulationListener clone() throws CloneNotSupportedException; + public SimulationListener clone(); } diff --git a/core/src/net/sf/openrocket/simulation/listeners/example/DampingMoment.java b/core/src/net/sf/openrocket/simulation/listeners/example/DampingMoment.java index 4cc5e9ed7..3e58beb5e 100644 --- a/core/src/net/sf/openrocket/simulation/listeners/example/DampingMoment.java +++ b/core/src/net/sf/openrocket/simulation/listeners/example/DampingMoment.java @@ -20,20 +20,7 @@ import net.sf.openrocket.util.Coordinate; public class DampingMoment extends AbstractSimulationListener { private static final FlightDataType type = FlightDataType.getType("Damping moment coefficient", "Cdm", UnitGroup.UNITS_COEFFICIENT); - private static final FlightDataType[] typeList = {type}; - - @Override - public String getName(){ - return "Damping moment listener"; - } - - /** - * Return a list of any flight data types this listener creates. - */ - @Override - public FlightDataType[] getFlightDataTypes(){ - return typeList; - } + private static final FlightDataType[] typeList = { type }; @Override public FlightConditions postFlightConditions(SimulationStatus status, FlightConditions flightConditions) throws SimulationException { @@ -42,11 +29,11 @@ public class DampingMoment extends AbstractSimulationListener { //status.getFlightData().setValue(type, aerodynamicPart + propulsivePart); status.getFlightData().setValue(type, calculate(status, flightConditions)); - + return flightConditions; } - private double calculate(SimulationStatus status, FlightConditions flightConditions){ + private double calculate(SimulationStatus status, FlightConditions flightConditions) { // Work out the propulsive/jet damping part of the moment. @@ -55,15 +42,15 @@ public class DampingMoment extends AbstractSimulationListener { List mpAll = data.get(FlightDataType.TYPE_PROPELLANT_MASS); List time = data.get(FlightDataType.TYPE_TIME); - if (mpAll == null || time == null){ + if (mpAll == null || time == null) { return Double.NaN; } int len = mpAll.size(); // This isn't as accurate as I would like - double mdot=Double.NaN; - if (len > 2){ + double mdot = Double.NaN; + if (len > 2) { // Using polynomial interpolator for derivative. Doesn't help much //double[] x = { time.get(len-5), time.get(len-4), time.get(len-3), time.get(len-2), time.get(len-1) }; //double[] y = { mpAll.get(len-5), mpAll.get(len-4), mpAll.get(len-3), mpAll.get(len-2), mpAll.get(len-1) }; @@ -71,22 +58,22 @@ public class DampingMoment extends AbstractSimulationListener { //double[] coeff = interp.interpolator(y); //double dt = .01; //mdot = (interp.eval(x[4], coeff) - interp.eval(x[4]-dt, coeff))/dt; - - mdot = (mpAll.get(len-1) - mpAll.get(len-2)) / (time.get(len-1) - time.get(len-2)); + + mdot = (mpAll.get(len - 1) - mpAll.get(len - 2)) / (time.get(len - 1) - time.get(len - 2)); } double cg = data.getLast(FlightDataType.TYPE_CG_LOCATION); // find the maximum distance from nose to nozzle. double nozzleDistance = 0; - for (MotorId id: status.getMotorConfiguration().getMotorIDs()){ + for (MotorId id : status.getMotorConfiguration().getMotorIDs()) { MotorInstanceConfiguration config = status.getMotorConfiguration(); Coordinate position = config.getMotorPosition(id); double x = position.x + config.getMotorInstance(id).getParentMotor().getLength(); - if (x > nozzleDistance){ + if (x > nozzleDistance) { nozzleDistance = x; - } + } } // now can get the propulsive part @@ -99,11 +86,12 @@ public class DampingMoment extends AbstractSimulationListener { // Must go through each component ... Map forces = aerocalc.getForceAnalysis(status.getConfiguration(), flightConditions, null); - for (Map.Entry entry : forces.entrySet()){ + for (Map.Entry entry : forces.entrySet()) { RocketComponent comp = entry.getKey(); - if (!comp.isAerodynamic()) continue; + if (!comp.isAerodynamic()) + continue; //System.out.println(comp.toString()); @@ -111,7 +99,7 @@ public class DampingMoment extends AbstractSimulationListener { double Cp = entry.getValue().getCP().length(); double z = comp.getPositionValue(); //? - aerodynamicPart += CNa*Math.pow(z-Cp, 2); + aerodynamicPart += CNa * Math.pow(z - Cp, 2); } double v = flightConditions.getVelocity(); @@ -121,7 +109,7 @@ public class DampingMoment extends AbstractSimulationListener { aerodynamicPart = aerodynamicPart * .5 * rho * v * ar; return aerodynamicPart + propulsivePart; - + } } diff --git a/core/src/net/sf/openrocket/startup/Application.java b/core/src/net/sf/openrocket/startup/Application.java index c9a5fdf82..13e8fdedf 100644 --- a/core/src/net/sf/openrocket/startup/Application.java +++ b/core/src/net/sf/openrocket/startup/Application.java @@ -4,6 +4,7 @@ import net.sf.openrocket.database.ComponentPresetDao; import net.sf.openrocket.database.motor.MotorDatabase; import net.sf.openrocket.database.motor.ThrustCurveMotorSetDatabase; import net.sf.openrocket.l10n.ClassBasedTranslator; +import net.sf.openrocket.l10n.DebugTranslator; import net.sf.openrocket.l10n.ExceptionSuppressingTranslator; import net.sf.openrocket.l10n.Translator; @@ -33,6 +34,10 @@ public final class Application { } private static Translator getBaseTranslator() { + if (injector == null) { + // Occurs in some unit tests + return new DebugTranslator(null); + } return injector.getInstance(Translator.class); } diff --git a/core/src/net/sf/openrocket/startup/Preferences.java b/core/src/net/sf/openrocket/startup/Preferences.java index 738a423f6..b134e8380 100644 --- a/core/src/net/sf/openrocket/startup/Preferences.java +++ b/core/src/net/sf/openrocket/startup/Preferences.java @@ -124,6 +124,8 @@ public abstract class Preferences implements ChangeSource { public abstract void putString(String directory, String key, String value); + public abstract java.util.prefs.Preferences getNode(String nodeName); + /* * ****************************************************************************************** */ diff --git a/core/src/net/sf/openrocket/unit/UnitGroup.java b/core/src/net/sf/openrocket/unit/UnitGroup.java index 1acf0a6ae..ae1a895b9 100644 --- a/core/src/net/sf/openrocket/unit/UnitGroup.java +++ b/core/src/net/sf/openrocket/unit/UnitGroup.java @@ -278,6 +278,7 @@ public class UnitGroup { UNITS_ROLL = new UnitGroup(); UNITS_ROLL.addUnit(new GeneralUnit(1, "rad/s")); + UNITS_ROLL.addUnit(new GeneralUnit(Math.PI / 180, DEGREE + "/s")); UNITS_ROLL.addUnit(new GeneralUnit(2 * Math.PI, "r/s")); UNITS_ROLL.addUnit(new GeneralUnit(2 * Math.PI / 60, "rpm")); UNITS_ROLL.setDefaultUnit(1); 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/src/net/sf/openrocket/util/TestRockets.java b/core/src/net/sf/openrocket/util/TestRockets.java index fedb59f4a..f4fff0762 100644 --- a/core/src/net/sf/openrocket/util/TestRockets.java +++ b/core/src/net/sf/openrocket/util/TestRockets.java @@ -48,6 +48,7 @@ import net.sf.openrocket.rocketcomponent.TubeCoupler; import net.sf.openrocket.simulation.SimulationOptions; import net.sf.openrocket.simulation.customexpression.CustomExpression; import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.simulation.extension.impl.ScriptingExtension; import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; import net.sf.openrocket.simulation.listeners.SimulationListener; import net.sf.openrocket.startup.Application; @@ -241,7 +242,7 @@ public class TestRockets { } - public Rocket makeSmallFlyable() { + public static Rocket makeSmallFlyable() { double noseconeLength = 0.10, noseconeRadius = 0.01; double bodytubeLength = 0.20, bodytubeRadius = 0.01, bodytubeThickness = 0.001; @@ -281,8 +282,12 @@ public class TestRockets { String id = rocket.newFlightConfigurationID(); bodytube.setMotorMount(true); - Motor m = Application.getMotorSetDatabase().findMotors(null, null, "B4", Double.NaN, Double.NaN).get(0); - bodytube.getMotorConfiguration().get(id).setMotor(m); + MotorConfiguration motorConfig = new MotorConfiguration(); + ThrustCurveMotor motor = getTestMotor(); + motorConfig.setMotor(motor); + motorConfig.setEjectionDelay(5); + + bodytube.getMotorConfiguration().set(id, motorConfig); bodytube.setMotorOverhang(0.005); rocket.getDefaultConfiguration().setFlightConfigurationID(id); @@ -643,11 +648,7 @@ public class TestRockets { // create motor config and add a motor to it MotorConfiguration motorConfig = new MotorConfiguration(); - ThrustCurveMotor motor = new ThrustCurveMotor( - Manufacturer.getManufacturer("A"), - "F12X", "Desc", Motor.Type.UNKNOWN, new double[] {}, - 0.024, 0.07, new double[] { 0, 1, 2 }, new double[] { 0, 1, 0 }, - new Coordinate[] { Coordinate.NUL, Coordinate.NUL, Coordinate.NUL }, "digestA"); + ThrustCurveMotor motor = getTestMotor(); motorConfig.setMotor(motor); motorConfig.setEjectionDelay(5); @@ -685,11 +686,7 @@ public class TestRockets { // create motor config and add a motor to it MotorConfiguration motorConfig = new MotorConfiguration(); - ThrustCurveMotor motor = new ThrustCurveMotor( - Manufacturer.getManufacturer("A"), - "F12X", "Desc", Motor.Type.UNKNOWN, new double[] {}, - 0.024, 0.07, new double[] { 0, 1, 2 }, new double[] { 0, 1, 0 }, - new Coordinate[] { Coordinate.NUL, Coordinate.NUL, Coordinate.NUL }, "digestA"); + ThrustCurveMotor motor = getTestMotor(); motorConfig.setMotor(motor); motorConfig.setEjectionDelay(5); @@ -918,6 +915,20 @@ public class TestRockets { return OpenRocketDocumentFactory.createDocumentFromRocket(rocket); } + + public static OpenRocketDocument makeTestRocket_v107_withSimulationExtension(String script) { + Rocket rocket = makeSmallFlyable(); + OpenRocketDocument document = OpenRocketDocumentFactory.createDocumentFromRocket(rocket); + Simulation sim = new Simulation(rocket); + ScriptingExtension ext = new ScriptingExtension(); + ext.setEnabled(true); + ext.setLanguage("JavaScript"); + ext.setScript(script); + sim.getSimulationExtensions().add(ext); + document.addSimulation(sim); + return document; + } + /* * Create a new test rocket for testing OpenRocketSaver.estimateFileSize() */ @@ -991,4 +1002,15 @@ public class TestRockets { } + + + private static ThrustCurveMotor getTestMotor() { + return new ThrustCurveMotor( + Manufacturer.getManufacturer("A"), + "F12X", "Desc", Motor.Type.UNKNOWN, new double[] {}, + 0.024, 0.07, new double[] { 0, 1, 2 }, new double[] { 0, 1, 0 }, + new Coordinate[] { Coordinate.NUL, Coordinate.NUL, Coordinate.NUL }, "digestA"); + } + + } diff --git a/core/test/net/sf/openrocket/ServicesForTesting.java b/core/test/net/sf/openrocket/ServicesForTesting.java index 2d6ec6743..8f729ffce 100644 --- a/core/test/net/sf/openrocket/ServicesForTesting.java +++ b/core/test/net/sf/openrocket/ServicesForTesting.java @@ -4,6 +4,7 @@ import java.util.Collections; import java.util.Locale; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import java.util.prefs.BackingStoreException; import net.sf.openrocket.formatting.RocketDescriptor; import net.sf.openrocket.formatting.RocketDescriptorImpl; @@ -62,6 +63,8 @@ public class ServicesForTesting extends AbstractModule { public static class PreferencesForTesting extends Preferences { + private static java.util.prefs.Preferences root = null; + @Override public boolean getBoolean(String key, boolean defaultValue) { // TODO Auto-generated method stub @@ -151,5 +154,28 @@ public class ServicesForTesting extends AbstractModule { return null; } + @Override + public java.util.prefs.Preferences getNode(String nodeName) { + return getBaseNode().node(nodeName); + } + + private java.util.prefs.Preferences getBaseNode() { + if (root == null) { + final String name = "OpenRocket-unittest-" + System.currentTimeMillis(); + root = java.util.prefs.Preferences.userRoot().node(name); + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + try { + root.removeNode(); + } catch (BackingStoreException e) { + e.printStackTrace(); + } + } + }); + } + return root; + } + } } diff --git a/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java b/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java index 96f826c08..ca18850a1 100644 --- a/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java +++ b/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java @@ -28,6 +28,8 @@ import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.motor.Motor; import net.sf.openrocket.motor.ThrustCurveMotor; import net.sf.openrocket.plugin.PluginModule; +import net.sf.openrocket.simulation.extension.impl.ScriptingExtension; +import net.sf.openrocket.simulation.extension.impl.ScriptingUtil; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.TestRockets; @@ -47,6 +49,10 @@ public class OpenRocketSaverTest { private OpenRocketSaver saver = new OpenRocketSaver(); private static final String TMP_DIR = "./tmp/"; + public static final String SIMULATION_EXTENSION_SCRIPT = "// Test < &\n// >\n// supportedVersions = Arrays.asList(DocumentConfig.SUPPORTED_VERSIONS); List testedVersions = Arrays.asList(testedVersionsStr); diff --git a/core/test/net/sf/openrocket/simulation/extension/impl/TestScriptingUtil.java b/core/test/net/sf/openrocket/simulation/extension/impl/TestScriptingUtil.java new file mode 100644 index 000000000..bd20a5fa8 --- /dev/null +++ b/core/test/net/sf/openrocket/simulation/extension/impl/TestScriptingUtil.java @@ -0,0 +1,88 @@ +package net.sf.openrocket.simulation.extension.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import net.sf.openrocket.startup.MockPreferences; + +import org.junit.Before; +import org.junit.Test; + +public class TestScriptingUtil { + + private static final String HASH_JavaScript_foobar = "SHA-256:8f06133e0235d239355b5ca8ca0b43dece803c29b2a563222519d982abd3fc43"; + + private ScriptingUtil util; + + @Before + public void setup() { + util = new ScriptingUtil(); + util.prefs = new MockPreferences(); + } + + /* + * Note: This class assumes that the JRE supports JavaScript scripting. + */ + + @Test + public void testGetLanguage() { + assertEquals(null, util.getLanguage(null)); + assertEquals(null, util.getLanguage("")); + assertEquals(null, util.getLanguage("foobar")); + assertEquals("JavaScript", util.getLanguage("JavaScript")); + assertEquals("JavaScript", util.getLanguage("javascript")); + assertEquals("JavaScript", util.getLanguage("ECMAScript")); + assertEquals("JavaScript", util.getLanguage("js")); + } + + + @Test + public void testGetLanguages() { + assertTrue(util.getLanguages().size() >= 1); + assertTrue(util.getLanguages().contains("JavaScript")); + } + + @Test + public void testIsTrustedScript() { + util.setTrustedScript("JavaScript", "foobar", true); + assertTrue(util.isTrustedScript("JavaScript", "foobar")); + assertTrue(util.isTrustedScript("JavaScript", " \n foobar \n\t\r")); + assertFalse(util.isTrustedScript("JavaScript", "foo\nbar")); + assertFalse(util.isTrustedScript("Javascript", "foobar")); + + // Empty script is always considered trusted + assertFalse(util.isTrustedScript("foo", null)); + assertTrue(util.isTrustedScript("foo", "")); + assertTrue(util.isTrustedScript("foo", " \n\r\t ")); + } + + @Test + public void testSetTrustedScript() { + util.setTrustedScript("JavaScript", " \n foobar \n\r ", true); + assertTrue(util.prefs.getNode(ScriptingUtil.NODE_ID).getBoolean(HASH_JavaScript_foobar, false)); + util.setTrustedScript("JavaScript", " foobar ", false); + assertTrue(util.prefs.getNode(ScriptingUtil.NODE_ID).getBoolean(HASH_JavaScript_foobar, true)); + assertFalse(util.prefs.getNode(ScriptingUtil.NODE_ID).getBoolean(HASH_JavaScript_foobar, false)); + } + + @Test + public void testClearTrustedScripts() { + util.setTrustedScript("JavaScript", "foobar", true); + assertTrue(util.isTrustedScript("JavaScript", "foobar")); + util.clearTrustedScripts(); + assertFalse(util.isTrustedScript("JavaScript", "foobar")); + } + + @Test + public void testNormalize() { + assertEquals("foo", ScriptingUtil.normalize("foo")); + assertEquals("foo bar", ScriptingUtil.normalize(" \n\r\t foo \r bar \n\t\r ")); + } + + @Test + public void testHash() { + assertEquals("SHA-256:12e6a78889b96a16d305b8e4af81119545f89eccba5fb37cc3a1ec2c53eab514", ScriptingUtil.hash("JS", "")); + assertEquals("SHA-256:000753e5deb2d8fa80e602ca03bcdb8e12a6b14b2b4a4d0abecdc976ad26e3ef", ScriptingUtil.hash("foo", "1165")); + assertEquals(HASH_JavaScript_foobar, ScriptingUtil.hash("JavaScript", "foobar")); + } +} diff --git a/core/test/net/sf/openrocket/startup/MockPreferences.java b/core/test/net/sf/openrocket/startup/MockPreferences.java new file mode 100644 index 000000000..eabfe18d9 --- /dev/null +++ b/core/test/net/sf/openrocket/startup/MockPreferences.java @@ -0,0 +1,108 @@ +package net.sf.openrocket.startup; + +import java.util.Set; +import java.util.prefs.BackingStoreException; + +import net.sf.openrocket.material.Material; +import net.sf.openrocket.preset.ComponentPreset; +import net.sf.openrocket.preset.ComponentPreset.Type; +import net.sf.openrocket.util.BugException; + +public class MockPreferences extends Preferences { + + private final String NODENAME = "OpenRocket-test-mock"; + private final java.util.prefs.Preferences NODE; + + public MockPreferences() { + java.util.prefs.Preferences root = java.util.prefs.Preferences.userRoot(); + try { + if (root.nodeExists(NODENAME)) { + root.node(NODENAME).removeNode(); + } + } catch (BackingStoreException e) { + throw new BugException("Unable to clear preference node", e); + } + NODE = root.node(NODENAME); + } + + @Override + public boolean getBoolean(String key, boolean def) { + return NODE.getBoolean(key, def); + } + + @Override + public void putBoolean(String key, boolean value) { + NODE.putBoolean(key, value); + } + + @Override + public int getInt(String key, int def) { + return NODE.getInt(key, def); + } + + @Override + public void putInt(String key, int value) { + NODE.putInt(key, value); + } + + @Override + public double getDouble(String key, double def) { + return NODE.getDouble(key, def); + } + + @Override + public void putDouble(String key, double value) { + NODE.putDouble(key, value); + } + + @Override + public String getString(String key, String def) { + return NODE.get(key, def); + } + + @Override + public void putString(String key, String value) { + NODE.put(key, value); + } + + @Override + public String getString(String directory, String key, String def) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public void putString(String directory, String key, String value) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public java.util.prefs.Preferences getNode(String nodeName) { + return NODE.node(nodeName); + } + + @Override + public void addUserMaterial(Material m) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public Set getUserMaterials() { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public void removeUserMaterial(Material m) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public void setComponentFavorite(ComponentPreset preset, Type type, boolean favorite) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public Set getComponentFavorites(Type type) { + throw new UnsupportedOperationException("Not yet implemented"); + } + +} 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..8305c734c 100644 --- a/swing/.classpath +++ b/swing/.classpath @@ -1,25 +1,26 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/swing/build.xml b/swing/build.xml index 6b01804e6..d6c4f9d10 100644 --- a/swing/build.xml +++ b/swing/build.xml @@ -105,6 +105,7 @@ + diff --git a/swing/lib/rsyntaxtextarea-2.5.6.jar b/swing/lib/rsyntaxtextarea-2.5.6.jar new file mode 100644 index 000000000..b60def66e Binary files /dev/null and b/swing/lib/rsyntaxtextarea-2.5.6.jar differ diff --git a/swing/resources/datafiles/examples/Simulation extensions and scripting.ork b/swing/resources/datafiles/examples/Simulation extensions and scripting.ork new file mode 100644 index 000000000..a1592fc11 Binary files /dev/null and b/swing/resources/datafiles/examples/Simulation extensions and scripting.ork differ diff --git a/swing/resources/datafiles/examples/Simulation listeners.ork b/swing/resources/datafiles/examples/Simulation listeners.ork deleted file mode 100644 index d4fbae528..000000000 Binary files a/swing/resources/datafiles/examples/Simulation listeners.ork and /dev/null differ diff --git a/swing/src/net/sf/openrocket/gui/simulation/SimulationEditDialog.java b/swing/src/net/sf/openrocket/gui/simulation/SimulationEditDialog.java index 53468bb61..7f8307225 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()); + } } } } @@ -167,7 +170,7 @@ public class SimulationEditDialog extends JDialog { //// Launch conditions tabbedPane.addTab(trans.get("simedtdlg.tab.Launchcond"), new SimulationConditionsPanel(simulation[0])); //// Simulation options - tabbedPane.addTab(trans.get("simedtdlg.tab.Simopt"), new SimulationOptionsPanel(simulation[0])); + tabbedPane.addTab(trans.get("simedtdlg.tab.Simopt"), new SimulationOptionsPanel(document, simulation[0])); tabbedPane.setSelectedIndex(0); diff --git a/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java b/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java index b94748b0f..6a925edef 100644 --- a/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java +++ b/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java @@ -1,54 +1,70 @@ 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; +import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.document.Simulation; import net.sf.openrocket.gui.SpinnerEditor; 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.startup.Preferences; 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(); - + + private OpenRocketDocument document; final Simulation simulation; - - SimulationOptionsPanel(final Simulation simulation) { + + private JPanel currentExtensions; + + SimulationOptionsPanel(OpenRocketDocument document, final Simulation simulation) { super(new MigLayout("fill")); + this.document = document; this.simulation = simulation; - + final SimulationOptions conditions = simulation.getOptions(); - + JPanel sub, subsub; String tip; JLabel label; @@ -56,7 +72,7 @@ class SimulationOptionsPanel extends JPanel { JSpinner spin; UnitSelector unit; BasicSlider slider; - + // // Simulation options sub = new JPanel(new MigLayout("fill, gap rel unrel", "[grow][65lp!][30lp!][75lp!]", "")); @@ -64,38 +80,38 @@ class SimulationOptionsPanel extends JPanel { sub.setBorder(BorderFactory.createTitledBorder(trans .get("simedtdlg.border.Simopt"))); this.add(sub, "growx, growy, aligny 0"); - + // Separate panel for computation methods, as they use a different // layout subsub = new JPanel(new MigLayout("insets 0, fill", "[grow][min!][min!][]")); - + // // Calculation method: tip = trans.get("simedtdlg.lbl.ttip.Calcmethod"); label = new JLabel(trans.get("simedtdlg.lbl.Calcmethod")); label.setToolTipText(tip); subsub.add(label, "gapright para"); - + // // Extended Barrowman label = new JLabel(trans.get("simedtdlg.lbl.ExtBarrowman")); label.setToolTipText(tip); subsub.add(label, "growx, span 3, wrap"); - + // Simulation method tip = trans.get("simedtdlg.lbl.ttip.Simmethod1") + trans.get("simedtdlg.lbl.ttip.Simmethod2"); label = new JLabel(trans.get("simedtdlg.lbl.Simmethod")); label.setToolTipText(tip); subsub.add(label, "gapright para"); - + label = new JLabel("6-DOF Runge-Kutta 4"); label.setToolTipText(tip); subsub.add(label, "growx, span 3, wrap"); - + // // Geodetic calculation method: label = new JLabel(trans.get("simedtdlg.lbl.GeodeticMethod")); label.setToolTipText(trans.get("simedtdlg.lbl.ttip.GeodeticMethodTip")); subsub.add(label, "gapright para"); - + EnumModel gcsModel = new EnumModel( conditions, "GeodeticComputation"); final JComboBox gcsCombo = new JComboBox(gcsModel); @@ -110,7 +126,7 @@ class SimulationOptionsPanel extends JPanel { gcsCombo.addActionListener(gcsTTipListener); gcsTTipListener.actionPerformed(null); subsub.add(gcsCombo, "span 3, wrap para"); - + // // Time step: label = new JLabel(trans.get("simedtdlg.lbl.Timestep")); @@ -121,25 +137,25 @@ class SimulationOptionsPanel extends JPanel { .toStringUnit(RK4SimulationStepper.RECOMMENDED_TIME_STEP) + "."; label.setToolTipText(tip); - subsub.add(label,"gapright para"); - + subsub.add(label, "gapright para"); + m = new DoubleModel(conditions, "TimeStep", UnitGroup.UNITS_TIME_STEP, 0, 1); - + spin = new JSpinner(m.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); spin.setToolTipText(tip); subsub.add(spin, ""); - + unit = new UnitSelector(m); unit.setToolTipText(tip); subsub.add(unit, ""); slider = new BasicSlider(m.getSliderModel(0, 0.2)); slider.setToolTipText(tip); subsub.add(slider, "w 100"); - + sub.add(subsub, "spanx, wrap para"); - + // Reset to default button JButton button = new JButton(trans.get("simedtdlg.but.resettodefault")); // Reset the time step to its default value ( @@ -159,141 +175,227 @@ class SimulationOptionsPanel extends JPanel { GeodeticComputationStrategy.SPHERICAL)); } }); - + sub.add(button, "align left"); - - // 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"); - - // // 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(); + desc.setText(trans.get("simedtdlg.SimExt.desc")); + sub.add(desc, "aligny 0, hmin 100lp, growx, wrap para"); + + + final JButton addExtension = new JButton(trans.get("simedtdlg.SimExt.add")); + 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"); - - // // 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]); + sub.add(addExtension, "growx, wrap 0"); + + 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(); + SwingSimulationExtensionConfigurator configurator = findConfigurator(e); + if (configurator != null) { + configurator.configure(e, simulation, SwingUtilities.windowForComponent(SimulationOptionsPanel.this)); + } + } + }); + menu.add(item); } - listenerModel.fireContentsChanged(); } + } + + JMenu copyMenu = null; + for (Simulation sim : document.getSimulations()) { + if (!sim.getSimulationExtensions().isEmpty()) { + JMenu menu = new JMenu(sim.getName()); + for (final SimulationExtension ext : sim.getSimulationExtensions()) { + JMenuItem item = new JMenuItem(ext.getName()); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + SimulationExtension e = ext.clone(); + simulation.getSimulationExtensions().add(e); + updateCurrentExtensions(); + SwingSimulationExtensionConfigurator configurator = findConfigurator(e); + if (configurator != null) { + configurator.configure(e, simulation, SwingUtilities.windowForComponent(SimulationOptionsPanel.this)); + } + } + }); + menu.add(item); + } + + if (copyMenu == null) { + copyMenu = new JMenu(trans.get("simedtdlg.SimExt.copyExtension")); + } + copyMenu.add(menu); + } + } + if (copyMenu != null) { + basemenu.add(copyMenu); + } + + return basemenu; + } + + 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(); + + 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 (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"); + } + + 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>() { }); - sub.add(button, "sizegroup buttons, alignx 50%"); - - } - - private class ListenerCellRenderer extends JLabel implements - ListCellRenderer { - - @Override - public Component getListCellRendererComponent(JList list, Object value, - int index, boolean isSelected, boolean cellHasFocus) { - 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; + for (SwingSimulationExtensionConfigurator c : configurators) { + if (c.support(extension)) { + return c; } - - 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; } + 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()); - } - } - + } 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 ff67daefa..48f1ccf0e 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); @@ -78,6 +78,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/gui/util/SwingPreferences.java b/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java index 499620c9b..b4bd15cdf 100644 --- a/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java +++ b/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java @@ -45,8 +45,8 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences { for (String lang : new String[] { "en", "de", "es", "fr", "it", "ru", "cs", "pl", "ja", "pt", "tr" }) { list.add(new Locale(lang)); } - list.add(new Locale("zh","CN")); - list.add(new Locale("uk","UA")); + list.add(new Locale("zh", "CN")); + list.add(new Locale("uk", "UA")); SUPPORTED_LOCALES = Collections.unmodifiableList(list); } @@ -200,6 +200,7 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences { * @param nodeName the node name * @return the preferences object for that node */ + @Override public Preferences getNode(String nodeName) { return PREFNODE.node(nodeName); } @@ -418,11 +419,11 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences { public boolean computeFlightInBackground() { return PREFNODE.getBoolean("backgroundFlight", true); } - + public void setComputeFlightInBackground(boolean b) { PREFNODE.putBoolean("backgroundFlight", b); } - + public Simulation getBackgroundSimulation(Rocket rocket) { Simulation s = new Simulation(rocket); SimulationOptions cond = s.getOptions(); 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..8eb2ea1ae --- /dev/null +++ b/swing/src/net/sf/openrocket/simulation/extension/AbstractSwingSimulationExtensionConfigurator.java @@ -0,0 +1,88 @@ +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; + + private JDialog dialog; + + 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) { + 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); + close(); + GUIUtil.setNullModels(dialog); + dialog = null; + } + + /** + * Return a title for the dialog window. By default uses the extension's name. + */ + protected String getTitle(SimulationExtension extension, Simulation simulation) { + return extension.getName(); + } + + /** + * Return the dialog currently open. + */ + protected JDialog getDialog() { + return dialog; + } + + /** + * Called when the default dialog is closed. By default does nothing. + */ + protected void close() { + + } + + 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..8b7879526 --- /dev/null +++ b/swing/src/net/sf/openrocket/simulation/extension/impl/AirStartConfigurator.java @@ -0,0 +1,58 @@ +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"); + + + panel.add(new JLabel("Launch velocity:")); + + m = new DoubleModel(extension, "LaunchVelocity", UnitGroup.UNITS_VELOCITY, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "w 65lp!"); + + unit = new UnitSelector(m); + panel.add(unit, "w 25"); + + slider = new BasicSlider(m.getSliderModel(0, 150)); + 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; + } + +} diff --git a/swing/src/net/sf/openrocket/simulation/extension/impl/ScriptingConfigurator.java b/swing/src/net/sf/openrocket/simulation/extension/impl/ScriptingConfigurator.java new file mode 100644 index 000000000..1005e600e --- /dev/null +++ b/swing/src/net/sf/openrocket/simulation/extension/impl/ScriptingConfigurator.java @@ -0,0 +1,166 @@ +package net.sf.openrocket.simulation.extension.impl; + +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.util.Set; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JOptionPane; +import javax.swing.JPanel; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.gui.adaptors.BooleanModel; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.components.StyledLabel.Style; +import net.sf.openrocket.plugin.Plugin; +import net.sf.openrocket.simulation.extension.AbstractSwingSimulationExtensionConfigurator; + +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.fife.ui.rsyntaxtextarea.SyntaxConstants; +import org.fife.ui.rsyntaxtextarea.TokenMakerFactory; +import org.fife.ui.rtextarea.RTextScrollPane; + +import com.google.inject.Inject; + +@Plugin +public class ScriptingConfigurator extends AbstractSwingSimulationExtensionConfigurator { + + @Inject + private ScriptingUtil util; + + private JComboBox languageSelector; + private RSyntaxTextArea text; + private JCheckBox trusted; + + private ScriptingExtension extension; + private Simulation simulation; + + public ScriptingConfigurator() { + super(ScriptingExtension.class); + } + + @Override + protected JComponent getConfigurationComponent(final ScriptingExtension extension, Simulation simulation, JPanel panel) { + this.extension = extension; + this.simulation = simulation; + + panel.add(new StyledLabel(trans.get("SimulationExtension.scripting.language.label"), Style.BOLD), "spanx, split"); + + String[] languages = util.getLanguages().toArray(new String[0]); + languageSelector = new JComboBox(languages); + languageSelector.setEditable(false); + languageSelector.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + setLanguage((String) languageSelector.getSelectedItem()); + } + }); + panel.add(languageSelector, "wrap para"); + + + text = new RSyntaxTextArea(extension.getScript(), 20, 80); + text.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT); + text.setCodeFoldingEnabled(true); + text.setLineWrap(true); + text.setWrapStyleWord(true); + text.setEditable(true); + text.setCurrentLineHighlightColor(new Color(255, 255, 230)); + text.addFocusListener(new FocusListener() { + @Override + public void focusGained(FocusEvent event) { + } + + @Override + public void focusLost(FocusEvent event) { + String str = text.getText(); + if (!extension.getScript().equals(str)) { + extension.setScript(str); + } + } + }); + + RTextScrollPane scroll = new RTextScrollPane(text); + panel.add(scroll, "spanx, grow, wrap para"); + + + BooleanModel enabled = new BooleanModel(extension, "Enabled"); + JCheckBox check = new JCheckBox(enabled); + check.setText(trans.get("SimulationExtension.scripting.text.enabled")); + check.setToolTipText(trans.get("SimulationExtension.scripting.text.enabled.ttip")); + panel.add(check, "spanx, wrap rel"); + + trusted = new JCheckBox(trans.get("SimulationExtension.scripting.text.trusted")); + trusted.setSelected(util.isTrustedScript(extension.getLanguage(), extension.getScript())); + panel.add(trusted, "spanx, split"); + + panel.add(new JPanel(), "growx"); + + JButton button = new JButton(trans.get("SimulationExtension.scripting.text.trusted.clear")); + button.setToolTipText(trans.get("SimulationExtension.scripting.text.trusted.clear.ttip")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + util.clearTrustedScripts(); + JOptionPane.showMessageDialog(getDialog(), trans.get("SimulationExtension.scripting.text.trusted.cleared"), + trans.get("SimulationExtension.scripting.text.trusted.cleared.title"), JOptionPane.INFORMATION_MESSAGE); + } + }); + panel.add(button, "wrap rel"); + + + StyledLabel label = new StyledLabel(trans.get("SimulationExtension.scripting.text.trusted.msg"), -1, Style.ITALIC); + panel.add(label); + + setLanguage(util.getLanguage(extension.getLanguage())); + + return panel; + } + + @Override + protected void close() { + util.setTrustedScript(extension.getLanguage(), extension.getScript(), trusted.isSelected()); + } + + + private void setLanguage(String language) { + if (language == null) { + language = ""; + } + if (!language.equals(languageSelector.getSelectedItem())) { + languageSelector.setSelectedItem(language); + } + extension.setLanguage(language); + text.setSyntaxEditingStyle(findSyntaxLanguage(language)); + getDialog().setTitle(getTitle(extension, simulation)); + } + + private String findSyntaxLanguage(String language) { + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName(language); + + if (engine != null) { + Set supported = TokenMakerFactory.getDefaultInstance().keySet(); + for (String type : engine.getFactory().getMimeTypes()) { + if (supported.contains(type)) { + return type; + } + for (String match : supported) { + if (match.contains("/" + language.toLowerCase())) { + return match; + } + } + } + } + + return SyntaxConstants.SYNTAX_STYLE_NONE; + } + +} diff --git a/swing/src/net/sf/openrocket/utils/Scripting.java b/swing/src/net/sf/openrocket/utils/Scripting.java new file mode 100644 index 000000000..ca1a2993d --- /dev/null +++ b/swing/src/net/sf/openrocket/utils/Scripting.java @@ -0,0 +1,38 @@ +package net.sf.openrocket.utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.script.ScriptEngineFactory; +import javax.script.ScriptEngineManager; + +import org.fife.ui.rsyntaxtextarea.TokenMakerFactory; + +public class Scripting { + + public static void main(String[] args) { + System.out.println("Scripting APIs:"); + + ScriptEngineManager manager = new ScriptEngineManager(); + for (ScriptEngineFactory factory : manager.getEngineFactories()) { + System.out.println(" engineName=" + factory.getEngineName() + + " engineVersion=" + factory.getEngineVersion() + + " languageName=" + factory.getLanguageName() + + " languageVersion=" + factory.getLanguageVersion() + + " names=" + factory.getNames() + + " mimeTypes=" + factory.getMimeTypes() + + " extensions=" + factory.getExtensions()); + } + System.out.println(); + + System.out.println("RSyntaxTextArea supported syntax languages:"); + TokenMakerFactory f = TokenMakerFactory.getDefaultInstance(); + List list = new ArrayList(f.keySet()); + Collections.sort(list); + for (String type : list) { + System.out.println(" " + type); + } + System.out.println(); + } +}
+ * By default, this method returns the canonical name of this class. + */ + @Override + public String getId() { + return this.getClass().getCanonicalName(); + } + + /** + * {@inheritDoc} + *
+ * By default, this method returns the name provided to the constructor. + */ + @Override + public String getName() { + return name; + } + + /** + * {@inheritDoc} + *
+ * By default, this method returns null. + */ + @Override + public String getDescription() { + return null; + } + + /** + * {@inheritDoc} + *
+ * By default, this method returns an empty list. + */ + @Override + public List getFlightDataTypes() { + return Collections.emptyList(); + } + + /** + * {@inheritDoc} + * + * By default, this method does nothing. + */ + @Override + public void documentLoaded(OpenRocketDocument document, Simulation simulation, WarningSet warnings) { + + } + + /** + * By default, returns a new object obtained by calling Object.clone() and + * cloning the config object. + */ + @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 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 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..f0b6c93c2 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/SimulationExtension.java @@ -0,0 +1,94 @@ +package net.sf.openrocket.simulation.extension; + +import java.util.List; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.Simulation; +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(); + + /** + * Called once for each simulation this extension is attached to when loading a document. + * This may perform necessary changes to the document at load time. + * + * @param document the loaded document + * @param simulation the simulation this extension is attached to + * @param warnings the document loading warnings + */ + public void documentLoaded(OpenRocketDocument document, Simulation simulation, WarningSet warnings); + + /** + * 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..e591685b3 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/AirStart.java @@ -0,0 +1,58 @@ +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; + if (getLaunchVelocity() > 0.01) { + name = trans.get("SimulationExtension.airstart.name.altvel"); + } else { + name = trans.get("SimulationExtension.airstart.name.alt"); + } + name = L10N.replace(name, "{alt}", UnitGroup.UNITS_DISTANCE.toStringUnit(getLaunchAltitude())); + name = L10N.replace(name, "{vel}", UnitGroup.UNITS_VELOCITY.toStringUnit(getLaunchVelocity())); + return name; + } + + public double getLaunchAltitude() { + return config.getDouble("launchAltitude", 0.0); + } + + public void setLaunchAltitude(double launchAltitude) { + config.put("launchAltitude", launchAltitude); + fireChangeEvent(); + } + + public double getLaunchVelocity() { + return config.getDouble("launchVelocity", 0.0); + } + + public void setLaunchVelocity(double launchVelocity) { + config.put("launchVelocity", launchVelocity); + fireChangeEvent(); + } + + + private class AirStartListener extends AbstractSimulationListener { + @Override + public void startSimulation(SimulationStatus status) throws SimulationException { + status.setRocketPosition(new Coordinate(0, 0, getLaunchAltitude())); + status.setRocketVelocity(status.getRocketOrientationQuaternion().rotate(new Coordinate(0, 0, getLaunchVelocity()))); + } + } +} 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/simulation/extension/impl/ScriptingExtension.java b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingExtension.java new file mode 100644 index 000000000..3569d2aae --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingExtension.java @@ -0,0 +1,112 @@ +package net.sf.openrocket.simulation.extension.impl; + +import javax.script.Invocable; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.l10n.L10N; +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 com.google.inject.Inject; + +public class ScriptingExtension extends AbstractSimulationExtension { + + private static final String DEFAULT_LANGUAGE = "JavaScript"; + + @Inject + private ScriptingUtil util; + + + public ScriptingExtension() { + setLanguage(DEFAULT_LANGUAGE); + setScript(""); + setEnabled(true); + } + + @Override + public String getName() { + String name = trans.get("SimulationExtension.scripting.name"); + name = L10N.replace(name, "{language}", getLanguage()); + return name; + } + + @Override + public String getDescription() { + return trans.get("SimulationExtension.scripting.desc"); + } + + @Override + public void documentLoaded(OpenRocketDocument document, Simulation simulation, WarningSet warnings) { + /* + * Scripts that the user has not explicitly indicated as trusted are disabled + * when loading from a file. This is to prevent trojans. + */ + if (isEnabled()) { + if (!util.isTrustedScript(getLanguage(), getScript())) { + setEnabled(false); + warnings.add(Warning.fromString(trans.get("SimulationExtension.scripting.warning.disabled"))); + } + } + } + + @Override + public void initialize(SimulationConditions conditions) throws SimulationException { + if (isEnabled()) { + conditions.getSimulationListenerList().add(getListener()); + } + } + + + public String getScript() { + return config.getString("script", ""); + } + + public void setScript(String script) { + config.put("script", script); + } + + public String getLanguage() { + return config.getString("language", DEFAULT_LANGUAGE); + } + + public void setLanguage(String language) { + config.put("language", language); + } + + public boolean isEnabled() { + return config.getBoolean("enabled", false); + } + + public void setEnabled(boolean enabled) { + config.put("enabled", enabled); + } + + + SimulationListener getListener() throws SimulationException { + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName(getLanguage()); + if (engine == null) { + throw new SimulationException("Your JRE does not support the scripting language '" + getLanguage() + "'"); + } + + try { + engine.eval(getScript()); + } catch (ScriptException e) { + throw new SimulationException("Invalid script: " + e.getMessage()); + } + + if (!(engine instanceof Invocable)) { + throw new SimulationException("The scripting language '" + getLanguage() + "' does not implement the Invocable interface"); + } + return new ScriptingSimulationListener((Invocable) engine); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingProvider.java b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingProvider.java new file mode 100644 index 000000000..911b47214 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingProvider.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 ScriptingProvider extends AbstractSimulationExtensionProvider { + + public ScriptingProvider() { + super(ScriptingExtension.class, "User code", "Scripts"); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingSimulationListener.java b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingSimulationListener.java new file mode 100644 index 000000000..5eab2dfa8 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingSimulationListener.java @@ -0,0 +1,227 @@ +package net.sf.openrocket.simulation.extension.impl; + +import java.util.HashSet; +import java.util.Set; + +import javax.script.Invocable; +import javax.script.ScriptException; + +import net.sf.openrocket.aerodynamics.AerodynamicForces; +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.models.atmosphere.AtmosphericConditions; +import net.sf.openrocket.motor.MotorId; +import net.sf.openrocket.motor.MotorInstance; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.rocketcomponent.RecoveryDevice; +import net.sf.openrocket.simulation.AccelerationData; +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.simulation.MassData; +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.simulation.exception.SimulationListenerException; +import net.sf.openrocket.simulation.listeners.SimulationComputationListener; +import net.sf.openrocket.simulation.listeners.SimulationEventListener; +import net.sf.openrocket.simulation.listeners.SimulationListener; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Coordinate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ScriptingSimulationListener implements SimulationListener, SimulationComputationListener, SimulationEventListener, Cloneable { + + private final static Logger logger = LoggerFactory.getLogger(ScriptingSimulationListener.class); + + /* + * NOTE: This class is used instead of using the scripting interface API + * so that unimplemented script methods are not called unnecessarily. + */ + + private Invocable invocable; + private Set missing = new HashSet(); + + + public ScriptingSimulationListener(Invocable invocable) { + this.invocable = invocable; + } + + + @Override + public boolean isSystemListener() { + return false; + } + + + @Override + public SimulationListener clone() { + try { + ScriptingSimulationListener clone = (ScriptingSimulationListener) super.clone(); + clone.missing = new HashSet(missing); + return clone; + } catch (CloneNotSupportedException e) { + throw new BugException(e); + } + } + + + + //// SimulationListener //// + + @Override + public void startSimulation(SimulationStatus status) throws SimulationException { + invoke(Void.class, null, "startSimulation", status); + } + + @Override + public void endSimulation(SimulationStatus status, SimulationException exception) { + try { + invoke(Void.class, null, "endSimulation", status, exception); + } catch (SimulationException e) { + } + } + + @Override + public boolean preStep(SimulationStatus status) throws SimulationException { + return invoke(Boolean.class, true, "preStep", status); + } + + @Override + public void postStep(SimulationStatus status) throws SimulationException { + invoke(Void.class, null, "postStep", status); + } + + + + //// SimulationEventListener //// + + @Override + public boolean addFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { + return invoke(Boolean.class, true, "addFlightEvent", status, event); + } + + @Override + public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { + return invoke(Boolean.class, true, "handleFlightEvent", status, event); + } + + @Override + public boolean motorIgnition(SimulationStatus status, MotorId motorId, MotorMount mount, MotorInstance instance) throws SimulationException { + return invoke(Boolean.class, true, "motorIgnition", status, motorId, mount, instance); + } + + @Override + public boolean recoveryDeviceDeployment(SimulationStatus status, RecoveryDevice recoveryDevice) throws SimulationException { + return invoke(Boolean.class, true, "recoveryDeviceDeployment", status, recoveryDevice); + } + + + + //// SimulationComputationListener //// + + @Override + public AccelerationData preAccelerationCalculation(SimulationStatus status) throws SimulationException { + return invoke(AccelerationData.class, null, "preAccelerationCalculation", status); + } + + @Override + public AerodynamicForces preAerodynamicCalculation(SimulationStatus status) throws SimulationException { + return invoke(AerodynamicForces.class, null, "preAerodynamicCalculation", status); + } + + @Override + public AtmosphericConditions preAtmosphericModel(SimulationStatus status) throws SimulationException { + return invoke(AtmosphericConditions.class, null, "preAtmosphericModel", status); + } + + @Override + public FlightConditions preFlightConditions(SimulationStatus status) throws SimulationException { + return invoke(FlightConditions.class, null, "preFlightConditions", status); + } + + @Override + public double preGravityModel(SimulationStatus status) throws SimulationException { + return invoke(Double.class, Double.NaN, "preGravityModel", status); + } + + @Override + public MassData preMassCalculation(SimulationStatus status) throws SimulationException { + return invoke(MassData.class, null, "preMassCalculation", status); + } + + @Override + public double preSimpleThrustCalculation(SimulationStatus status) throws SimulationException { + return invoke(Double.class, Double.NaN, "preSimpleThrustCalculation", status); + } + + @Override + public Coordinate preWindModel(SimulationStatus status) throws SimulationException { + return invoke(Coordinate.class, null, "preWindModel", status); + } + + @Override + public AccelerationData postAccelerationCalculation(SimulationStatus status, AccelerationData acceleration) throws SimulationException { + return invoke(AccelerationData.class, null, "postAccelerationCalculation", status, acceleration); + } + + @Override + public AerodynamicForces postAerodynamicCalculation(SimulationStatus status, AerodynamicForces forces) throws SimulationException { + return invoke(AerodynamicForces.class, null, "postAerodynamicCalculation", status, forces); + } + + @Override + public AtmosphericConditions postAtmosphericModel(SimulationStatus status, AtmosphericConditions atmosphericConditions) throws SimulationException { + return invoke(AtmosphericConditions.class, null, "postAtmosphericModel", status, atmosphericConditions); + } + + @Override + public FlightConditions postFlightConditions(SimulationStatus status, FlightConditions flightConditions) throws SimulationException { + return invoke(FlightConditions.class, null, "postFlightConditions", status, flightConditions); + } + + @Override + public double postGravityModel(SimulationStatus status, double gravity) throws SimulationException { + return invoke(Double.class, Double.NaN, "postGravityModel", status, gravity); + } + + @Override + public MassData postMassCalculation(SimulationStatus status, MassData massData) throws SimulationException { + return invoke(MassData.class, null, "postMassCalculation", status, massData); + } + + @Override + public double postSimpleThrustCalculation(SimulationStatus status, double thrust) throws SimulationException { + return invoke(Double.class, Double.NaN, "postSimpleThrustCalculation", status, thrust); + } + + @Override + public Coordinate postWindModel(SimulationStatus status, Coordinate wind) throws SimulationException { + return invoke(Coordinate.class, null, "postWindModel", status, wind); + } + + + @SuppressWarnings("unchecked") + private T invoke(Class retType, T def, String method, Object... args) throws SimulationException { + try { + if (!missing.contains(method)) { + Object o = invocable.invokeFunction(method, args); + if (o == null) { + // Use default/null if function returns nothing + return def; + } else if (!o.getClass().equals(retType)) { + throw new SimulationListenerException("Custom script function " + method + " returned type " + + o.getClass().getSimpleName() + ", expected " + retType.getSimpleName()); + } else { + return (T) o; + } + } + } catch (NoSuchMethodException e) { + missing.add(method); + // fall-through + } catch (ScriptException e) { + logger.warn("Script exception in " + method + ": " + e, e); + throw new SimulationException("Script failed: " + e.getMessage()); + } + return def; + } + +} diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingUtil.java b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingUtil.java new file mode 100644 index 000000000..6f0f76517 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingUtil.java @@ -0,0 +1,158 @@ +package net.sf.openrocket.simulation.extension.impl; + +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.List; +import java.util.prefs.BackingStoreException; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; +import javax.script.ScriptEngineManager; + +import net.sf.openrocket.startup.Preferences; +import net.sf.openrocket.util.ArrayList; +import net.sf.openrocket.util.BugException; + +import com.google.inject.Inject; + +/** + * Utility class used by the scripting extension and its configurator. + */ +public class ScriptingUtil { + + static final String NODE_ID = ScriptingExtension.class.getCanonicalName(); + + private static final List DEFAULT_TRUSTED_HASHES = Arrays.asList( + // Roll control script in roll control example file: + "SHA-256:9bf364ce4d4a75f09b29178bf9d6872b232084f73dae20dc7b5b073e54e95a42" + ); + + /** The name to be chosen from a list of alternatives. If not found, will use the default name. */ + private static final List PREFERRED_LANGUAGE_NAMES = Arrays.asList("JavaScript"); + + @Inject + Preferences prefs; + + + + + /** + * Return the preferred internal language name based on a script language name. + * + * @return the preferred language name, or null if the language is not supported. + */ + public String getLanguage(String language) { + if (language == null) { + return null; + } + + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName(language); + if (engine == null) { + return null; + } + return getLanguage(engine.getFactory()); + } + + + public List getLanguages() { + List langs = new ArrayList(); + ScriptEngineManager manager = new ScriptEngineManager(); + for (ScriptEngineFactory factory : manager.getEngineFactories()) { + langs.add(getLanguage(factory)); + } + return langs; + } + + + private String getLanguage(ScriptEngineFactory factory) { + for (String name : factory.getNames()) { + if (PREFERRED_LANGUAGE_NAMES.contains(name)) { + return name; + } + } + + return factory.getLanguageName(); + } + + + + /** + * Test whether the user has indicated this script to be trusted, + * or if it is an internally trusted script. + */ + public boolean isTrustedScript(String language, String script) { + if (language == null || script == null) { + return false; + } + script = normalize(script); + if (script.length() == 0) { + return true; + } + String hash = hash(language, script); + if (DEFAULT_TRUSTED_HASHES.contains(hash)) { + return true; + } + return prefs.getNode(NODE_ID).getBoolean(hash, false); + } + + /** + * Mark a script as trusted. + */ + public void setTrustedScript(String language, String script, boolean trusted) { + script = normalize(script); + String hash = hash(language, script); + if (trusted) { + prefs.getNode(NODE_ID).putBoolean(hash, true); + } else { + prefs.getNode(NODE_ID).remove(hash); + } + } + + /** + * Clear all trusted scripts. + */ + public void clearTrustedScripts() { + try { + prefs.getNode(NODE_ID).clear(); + } catch (BackingStoreException e) { + throw new BugException(e); + } + } + + + static String normalize(String script) { + return script.replaceAll("\r", "").trim(); + } + + static String hash(String language, String script) { + /* + * NOTE: Hash length must be max 80 chars, the max length of a key in a Properties object. + */ + + String output; + MessageDigest digest; + + try { + digest = MessageDigest.getInstance("SHA-256"); + digest.update(language.getBytes("UTF-8")); + digest.update((byte) '|'); + byte[] hash = digest.digest(script.getBytes("UTF-8")); + BigInteger bigInt = new BigInteger(1, hash); + output = bigInt.toString(16); + while (output.length() < 64) { + output = "0" + output; + } + } catch (NoSuchAlgorithmException e) { + throw new BugException("JRE does not support SHA-256 hash algorithm", e); + } catch (UnsupportedEncodingException e) { + throw new BugException(e); + } + + return digest.getAlgorithm() + ":" + output; + } + +} diff --git a/core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java b/core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java index 0c8a3d32d..1ad3ef4ad 100644 --- a/core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java +++ b/core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java @@ -8,11 +8,11 @@ import net.sf.openrocket.motor.MotorInstance; import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.rocketcomponent.RecoveryDevice; import net.sf.openrocket.simulation.AccelerationData; -import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.FlightEvent; import net.sf.openrocket.simulation.MassData; import net.sf.openrocket.simulation.SimulationStatus; import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.Coordinate; @@ -24,20 +24,10 @@ import net.sf.openrocket.util.Coordinate; * @author Sampo Niskanen */ public class AbstractSimulationListener implements SimulationListener, SimulationComputationListener, - SimulationEventListener { + SimulationEventListener, Cloneable { //// SimulationListener //// - @Override - public String getName() { - return this.getClass().getSimpleName(); - } - - @Override - public String[] getMenuPosition() { - return new String[0]; - } - @Override public void startSimulation(SimulationStatus status) throws SimulationException { // No-op @@ -68,14 +58,6 @@ public class AbstractSimulationListener implements SimulationListener, Simulatio return false; } - /** - * Return an array of any flight data types this listener creates. - */ - @Override - public FlightDataType[] getFlightDataTypes() { - return new FlightDataType[] {}; - } - //// SimulationEventListener //// @@ -184,8 +166,12 @@ public class AbstractSimulationListener implements SimulationListener, Simulatio } @Override - public AbstractSimulationListener clone() throws CloneNotSupportedException { - return (AbstractSimulationListener) super.clone(); + public AbstractSimulationListener clone() { + try { + return (AbstractSimulationListener) super.clone(); + } catch (CloneNotSupportedException e) { + throw new BugException(e); + } } } diff --git a/core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java b/core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java index c02b25765..88422d72c 100644 --- a/core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java +++ b/core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java @@ -4,7 +4,6 @@ import net.sf.openrocket.aerodynamics.AerodynamicForces; import net.sf.openrocket.aerodynamics.FlightConditions; import net.sf.openrocket.models.atmosphere.AtmosphericConditions; import net.sf.openrocket.simulation.AccelerationData; -import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.MassData; import net.sf.openrocket.simulation.SimulationStatus; import net.sf.openrocket.simulation.exception.SimulationException; @@ -17,20 +16,20 @@ import net.sf.openrocket.util.Coordinate; * @author Sampo Niskanen */ public interface SimulationComputationListener extends SimulationListener { - - + + //////// Computation/modeling related callbacks //////// public AccelerationData preAccelerationCalculation(SimulationStatus status) throws SimulationException; public AccelerationData postAccelerationCalculation(SimulationStatus status, AccelerationData acceleration) - throws SimulationException; + throws SimulationException; public AtmosphericConditions preAtmosphericModel(SimulationStatus status) - throws SimulationException; + throws SimulationException; public AtmosphericConditions postAtmosphericModel(SimulationStatus status, AtmosphericConditions atmosphericConditions) - throws SimulationException; + throws SimulationException; public Coordinate preWindModel(SimulationStatus status) throws SimulationException; @@ -42,29 +41,27 @@ public interface SimulationComputationListener extends SimulationListener { public double postGravityModel(SimulationStatus status, double gravity) throws SimulationException; - + public FlightConditions preFlightConditions(SimulationStatus status) - throws SimulationException; + throws SimulationException; public FlightConditions postFlightConditions(SimulationStatus status, FlightConditions flightConditions) - throws SimulationException; + throws SimulationException; public AerodynamicForces preAerodynamicCalculation(SimulationStatus status) - throws SimulationException; + throws SimulationException; public AerodynamicForces postAerodynamicCalculation(SimulationStatus status, AerodynamicForces forces) - throws SimulationException; + throws SimulationException; public MassData preMassCalculation(SimulationStatus status) throws SimulationException; public MassData postMassCalculation(SimulationStatus status, MassData massData) throws SimulationException; - - + + public double preSimpleThrustCalculation(SimulationStatus status) throws SimulationException; public double postSimpleThrustCalculation(SimulationStatus status, double thrust) throws SimulationException; - - @Override - public FlightDataType[] getFlightDataTypes(); + } diff --git a/core/src/net/sf/openrocket/simulation/listeners/SimulationEventListener.java b/core/src/net/sf/openrocket/simulation/listeners/SimulationEventListener.java index 00ef6b7b5..300fb8645 100644 --- a/core/src/net/sf/openrocket/simulation/listeners/SimulationEventListener.java +++ b/core/src/net/sf/openrocket/simulation/listeners/SimulationEventListener.java @@ -4,14 +4,13 @@ import net.sf.openrocket.motor.MotorId; import net.sf.openrocket.motor.MotorInstance; import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.rocketcomponent.RecoveryDevice; -import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.FlightEvent; import net.sf.openrocket.simulation.SimulationStatus; import net.sf.openrocket.simulation.exception.SimulationException; public interface SimulationEventListener { - + /** * Called before adding a flight event to the event queue. * @@ -23,7 +22,7 @@ public interface SimulationEventListener { public boolean addFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException; - + /** * Called before handling a flight event. * @@ -57,10 +56,6 @@ public interface SimulationEventListener { */ public boolean recoveryDeviceDeployment(SimulationStatus status, RecoveryDevice recoveryDevice) throws SimulationException; - - - - public FlightDataType[] getFlightDataTypes(); - + } diff --git a/core/src/net/sf/openrocket/simulation/listeners/SimulationListener.java b/core/src/net/sf/openrocket/simulation/listeners/SimulationListener.java index bdbf90ffc..a0d4b147c 100644 --- a/core/src/net/sf/openrocket/simulation/listeners/SimulationListener.java +++ b/core/src/net/sf/openrocket/simulation/listeners/SimulationListener.java @@ -1,6 +1,5 @@ package net.sf.openrocket.simulation.listeners; -import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.SimulationStatus; import net.sf.openrocket.simulation.exception.SimulationException; @@ -10,26 +9,7 @@ import net.sf.openrocket.simulation.exception.SimulationException; * If the implementation maintains any state, it should be properly cloned. * */ -public interface SimulationListener extends Cloneable { - - /** - * Get the name of this simulation listener. Ideally this should be localized, as - * it can be displayed in the UI. - * - * @return the name of this simulation listener. - */ - public String getName(); - - - /** - * Get the menu position of this simulation listener. This should be an array - * of localized submenu names in descending order, or an empty array for positioning - * in the base menu. - * - * @return the menu position of this simulation listener. - */ - public String[] getMenuPosition(); - +public interface SimulationListener { /** * Called when starting a simulation. @@ -83,9 +63,7 @@ public interface SimulationListener extends Cloneable { /** - * Return a list of any flight data types this listener creates. + * Return a deep copy of this simulation listener including its state. */ - public FlightDataType[] getFlightDataTypes(); - - public SimulationListener clone() throws CloneNotSupportedException; + public SimulationListener clone(); } diff --git a/core/src/net/sf/openrocket/simulation/listeners/example/DampingMoment.java b/core/src/net/sf/openrocket/simulation/listeners/example/DampingMoment.java index 4cc5e9ed7..3e58beb5e 100644 --- a/core/src/net/sf/openrocket/simulation/listeners/example/DampingMoment.java +++ b/core/src/net/sf/openrocket/simulation/listeners/example/DampingMoment.java @@ -20,20 +20,7 @@ import net.sf.openrocket.util.Coordinate; public class DampingMoment extends AbstractSimulationListener { private static final FlightDataType type = FlightDataType.getType("Damping moment coefficient", "Cdm", UnitGroup.UNITS_COEFFICIENT); - private static final FlightDataType[] typeList = {type}; - - @Override - public String getName(){ - return "Damping moment listener"; - } - - /** - * Return a list of any flight data types this listener creates. - */ - @Override - public FlightDataType[] getFlightDataTypes(){ - return typeList; - } + private static final FlightDataType[] typeList = { type }; @Override public FlightConditions postFlightConditions(SimulationStatus status, FlightConditions flightConditions) throws SimulationException { @@ -42,11 +29,11 @@ public class DampingMoment extends AbstractSimulationListener { //status.getFlightData().setValue(type, aerodynamicPart + propulsivePart); status.getFlightData().setValue(type, calculate(status, flightConditions)); - + return flightConditions; } - private double calculate(SimulationStatus status, FlightConditions flightConditions){ + private double calculate(SimulationStatus status, FlightConditions flightConditions) { // Work out the propulsive/jet damping part of the moment. @@ -55,15 +42,15 @@ public class DampingMoment extends AbstractSimulationListener { List mpAll = data.get(FlightDataType.TYPE_PROPELLANT_MASS); List time = data.get(FlightDataType.TYPE_TIME); - if (mpAll == null || time == null){ + if (mpAll == null || time == null) { return Double.NaN; } int len = mpAll.size(); // This isn't as accurate as I would like - double mdot=Double.NaN; - if (len > 2){ + double mdot = Double.NaN; + if (len > 2) { // Using polynomial interpolator for derivative. Doesn't help much //double[] x = { time.get(len-5), time.get(len-4), time.get(len-3), time.get(len-2), time.get(len-1) }; //double[] y = { mpAll.get(len-5), mpAll.get(len-4), mpAll.get(len-3), mpAll.get(len-2), mpAll.get(len-1) }; @@ -71,22 +58,22 @@ public class DampingMoment extends AbstractSimulationListener { //double[] coeff = interp.interpolator(y); //double dt = .01; //mdot = (interp.eval(x[4], coeff) - interp.eval(x[4]-dt, coeff))/dt; - - mdot = (mpAll.get(len-1) - mpAll.get(len-2)) / (time.get(len-1) - time.get(len-2)); + + mdot = (mpAll.get(len - 1) - mpAll.get(len - 2)) / (time.get(len - 1) - time.get(len - 2)); } double cg = data.getLast(FlightDataType.TYPE_CG_LOCATION); // find the maximum distance from nose to nozzle. double nozzleDistance = 0; - for (MotorId id: status.getMotorConfiguration().getMotorIDs()){ + for (MotorId id : status.getMotorConfiguration().getMotorIDs()) { MotorInstanceConfiguration config = status.getMotorConfiguration(); Coordinate position = config.getMotorPosition(id); double x = position.x + config.getMotorInstance(id).getParentMotor().getLength(); - if (x > nozzleDistance){ + if (x > nozzleDistance) { nozzleDistance = x; - } + } } // now can get the propulsive part @@ -99,11 +86,12 @@ public class DampingMoment extends AbstractSimulationListener { // Must go through each component ... Map forces = aerocalc.getForceAnalysis(status.getConfiguration(), flightConditions, null); - for (Map.Entry entry : forces.entrySet()){ + for (Map.Entry entry : forces.entrySet()) { RocketComponent comp = entry.getKey(); - if (!comp.isAerodynamic()) continue; + if (!comp.isAerodynamic()) + continue; //System.out.println(comp.toString()); @@ -111,7 +99,7 @@ public class DampingMoment extends AbstractSimulationListener { double Cp = entry.getValue().getCP().length(); double z = comp.getPositionValue(); //? - aerodynamicPart += CNa*Math.pow(z-Cp, 2); + aerodynamicPart += CNa * Math.pow(z - Cp, 2); } double v = flightConditions.getVelocity(); @@ -121,7 +109,7 @@ public class DampingMoment extends AbstractSimulationListener { aerodynamicPart = aerodynamicPart * .5 * rho * v * ar; return aerodynamicPart + propulsivePart; - + } } diff --git a/core/src/net/sf/openrocket/startup/Application.java b/core/src/net/sf/openrocket/startup/Application.java index c9a5fdf82..13e8fdedf 100644 --- a/core/src/net/sf/openrocket/startup/Application.java +++ b/core/src/net/sf/openrocket/startup/Application.java @@ -4,6 +4,7 @@ import net.sf.openrocket.database.ComponentPresetDao; import net.sf.openrocket.database.motor.MotorDatabase; import net.sf.openrocket.database.motor.ThrustCurveMotorSetDatabase; import net.sf.openrocket.l10n.ClassBasedTranslator; +import net.sf.openrocket.l10n.DebugTranslator; import net.sf.openrocket.l10n.ExceptionSuppressingTranslator; import net.sf.openrocket.l10n.Translator; @@ -33,6 +34,10 @@ public final class Application { } private static Translator getBaseTranslator() { + if (injector == null) { + // Occurs in some unit tests + return new DebugTranslator(null); + } return injector.getInstance(Translator.class); } diff --git a/core/src/net/sf/openrocket/startup/Preferences.java b/core/src/net/sf/openrocket/startup/Preferences.java index 738a423f6..b134e8380 100644 --- a/core/src/net/sf/openrocket/startup/Preferences.java +++ b/core/src/net/sf/openrocket/startup/Preferences.java @@ -124,6 +124,8 @@ public abstract class Preferences implements ChangeSource { public abstract void putString(String directory, String key, String value); + public abstract java.util.prefs.Preferences getNode(String nodeName); + /* * ****************************************************************************************** */ diff --git a/core/src/net/sf/openrocket/unit/UnitGroup.java b/core/src/net/sf/openrocket/unit/UnitGroup.java index 1acf0a6ae..ae1a895b9 100644 --- a/core/src/net/sf/openrocket/unit/UnitGroup.java +++ b/core/src/net/sf/openrocket/unit/UnitGroup.java @@ -278,6 +278,7 @@ public class UnitGroup { UNITS_ROLL = new UnitGroup(); UNITS_ROLL.addUnit(new GeneralUnit(1, "rad/s")); + UNITS_ROLL.addUnit(new GeneralUnit(Math.PI / 180, DEGREE + "/s")); UNITS_ROLL.addUnit(new GeneralUnit(2 * Math.PI, "r/s")); UNITS_ROLL.addUnit(new GeneralUnit(2 * Math.PI / 60, "rpm")); UNITS_ROLL.setDefaultUnit(1); 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/src/net/sf/openrocket/util/TestRockets.java b/core/src/net/sf/openrocket/util/TestRockets.java index fedb59f4a..f4fff0762 100644 --- a/core/src/net/sf/openrocket/util/TestRockets.java +++ b/core/src/net/sf/openrocket/util/TestRockets.java @@ -48,6 +48,7 @@ import net.sf.openrocket.rocketcomponent.TubeCoupler; import net.sf.openrocket.simulation.SimulationOptions; import net.sf.openrocket.simulation.customexpression.CustomExpression; import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.simulation.extension.impl.ScriptingExtension; import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; import net.sf.openrocket.simulation.listeners.SimulationListener; import net.sf.openrocket.startup.Application; @@ -241,7 +242,7 @@ public class TestRockets { } - public Rocket makeSmallFlyable() { + public static Rocket makeSmallFlyable() { double noseconeLength = 0.10, noseconeRadius = 0.01; double bodytubeLength = 0.20, bodytubeRadius = 0.01, bodytubeThickness = 0.001; @@ -281,8 +282,12 @@ public class TestRockets { String id = rocket.newFlightConfigurationID(); bodytube.setMotorMount(true); - Motor m = Application.getMotorSetDatabase().findMotors(null, null, "B4", Double.NaN, Double.NaN).get(0); - bodytube.getMotorConfiguration().get(id).setMotor(m); + MotorConfiguration motorConfig = new MotorConfiguration(); + ThrustCurveMotor motor = getTestMotor(); + motorConfig.setMotor(motor); + motorConfig.setEjectionDelay(5); + + bodytube.getMotorConfiguration().set(id, motorConfig); bodytube.setMotorOverhang(0.005); rocket.getDefaultConfiguration().setFlightConfigurationID(id); @@ -643,11 +648,7 @@ public class TestRockets { // create motor config and add a motor to it MotorConfiguration motorConfig = new MotorConfiguration(); - ThrustCurveMotor motor = new ThrustCurveMotor( - Manufacturer.getManufacturer("A"), - "F12X", "Desc", Motor.Type.UNKNOWN, new double[] {}, - 0.024, 0.07, new double[] { 0, 1, 2 }, new double[] { 0, 1, 0 }, - new Coordinate[] { Coordinate.NUL, Coordinate.NUL, Coordinate.NUL }, "digestA"); + ThrustCurveMotor motor = getTestMotor(); motorConfig.setMotor(motor); motorConfig.setEjectionDelay(5); @@ -685,11 +686,7 @@ public class TestRockets { // create motor config and add a motor to it MotorConfiguration motorConfig = new MotorConfiguration(); - ThrustCurveMotor motor = new ThrustCurveMotor( - Manufacturer.getManufacturer("A"), - "F12X", "Desc", Motor.Type.UNKNOWN, new double[] {}, - 0.024, 0.07, new double[] { 0, 1, 2 }, new double[] { 0, 1, 0 }, - new Coordinate[] { Coordinate.NUL, Coordinate.NUL, Coordinate.NUL }, "digestA"); + ThrustCurveMotor motor = getTestMotor(); motorConfig.setMotor(motor); motorConfig.setEjectionDelay(5); @@ -918,6 +915,20 @@ public class TestRockets { return OpenRocketDocumentFactory.createDocumentFromRocket(rocket); } + + public static OpenRocketDocument makeTestRocket_v107_withSimulationExtension(String script) { + Rocket rocket = makeSmallFlyable(); + OpenRocketDocument document = OpenRocketDocumentFactory.createDocumentFromRocket(rocket); + Simulation sim = new Simulation(rocket); + ScriptingExtension ext = new ScriptingExtension(); + ext.setEnabled(true); + ext.setLanguage("JavaScript"); + ext.setScript(script); + sim.getSimulationExtensions().add(ext); + document.addSimulation(sim); + return document; + } + /* * Create a new test rocket for testing OpenRocketSaver.estimateFileSize() */ @@ -991,4 +1002,15 @@ public class TestRockets { } + + + private static ThrustCurveMotor getTestMotor() { + return new ThrustCurveMotor( + Manufacturer.getManufacturer("A"), + "F12X", "Desc", Motor.Type.UNKNOWN, new double[] {}, + 0.024, 0.07, new double[] { 0, 1, 2 }, new double[] { 0, 1, 0 }, + new Coordinate[] { Coordinate.NUL, Coordinate.NUL, Coordinate.NUL }, "digestA"); + } + + } diff --git a/core/test/net/sf/openrocket/ServicesForTesting.java b/core/test/net/sf/openrocket/ServicesForTesting.java index 2d6ec6743..8f729ffce 100644 --- a/core/test/net/sf/openrocket/ServicesForTesting.java +++ b/core/test/net/sf/openrocket/ServicesForTesting.java @@ -4,6 +4,7 @@ import java.util.Collections; import java.util.Locale; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import java.util.prefs.BackingStoreException; import net.sf.openrocket.formatting.RocketDescriptor; import net.sf.openrocket.formatting.RocketDescriptorImpl; @@ -62,6 +63,8 @@ public class ServicesForTesting extends AbstractModule { public static class PreferencesForTesting extends Preferences { + private static java.util.prefs.Preferences root = null; + @Override public boolean getBoolean(String key, boolean defaultValue) { // TODO Auto-generated method stub @@ -151,5 +154,28 @@ public class ServicesForTesting extends AbstractModule { return null; } + @Override + public java.util.prefs.Preferences getNode(String nodeName) { + return getBaseNode().node(nodeName); + } + + private java.util.prefs.Preferences getBaseNode() { + if (root == null) { + final String name = "OpenRocket-unittest-" + System.currentTimeMillis(); + root = java.util.prefs.Preferences.userRoot().node(name); + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + try { + root.removeNode(); + } catch (BackingStoreException e) { + e.printStackTrace(); + } + } + }); + } + return root; + } + } } diff --git a/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java b/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java index 96f826c08..ca18850a1 100644 --- a/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java +++ b/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java @@ -28,6 +28,8 @@ import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.motor.Motor; import net.sf.openrocket.motor.ThrustCurveMotor; import net.sf.openrocket.plugin.PluginModule; +import net.sf.openrocket.simulation.extension.impl.ScriptingExtension; +import net.sf.openrocket.simulation.extension.impl.ScriptingUtil; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.TestRockets; @@ -47,6 +49,10 @@ public class OpenRocketSaverTest { private OpenRocketSaver saver = new OpenRocketSaver(); private static final String TMP_DIR = "./tmp/"; + public static final String SIMULATION_EXTENSION_SCRIPT = "// Test < &\n// >\n// supportedVersions = Arrays.asList(DocumentConfig.SUPPORTED_VERSIONS); List testedVersions = Arrays.asList(testedVersionsStr); diff --git a/core/test/net/sf/openrocket/simulation/extension/impl/TestScriptingUtil.java b/core/test/net/sf/openrocket/simulation/extension/impl/TestScriptingUtil.java new file mode 100644 index 000000000..bd20a5fa8 --- /dev/null +++ b/core/test/net/sf/openrocket/simulation/extension/impl/TestScriptingUtil.java @@ -0,0 +1,88 @@ +package net.sf.openrocket.simulation.extension.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import net.sf.openrocket.startup.MockPreferences; + +import org.junit.Before; +import org.junit.Test; + +public class TestScriptingUtil { + + private static final String HASH_JavaScript_foobar = "SHA-256:8f06133e0235d239355b5ca8ca0b43dece803c29b2a563222519d982abd3fc43"; + + private ScriptingUtil util; + + @Before + public void setup() { + util = new ScriptingUtil(); + util.prefs = new MockPreferences(); + } + + /* + * Note: This class assumes that the JRE supports JavaScript scripting. + */ + + @Test + public void testGetLanguage() { + assertEquals(null, util.getLanguage(null)); + assertEquals(null, util.getLanguage("")); + assertEquals(null, util.getLanguage("foobar")); + assertEquals("JavaScript", util.getLanguage("JavaScript")); + assertEquals("JavaScript", util.getLanguage("javascript")); + assertEquals("JavaScript", util.getLanguage("ECMAScript")); + assertEquals("JavaScript", util.getLanguage("js")); + } + + + @Test + public void testGetLanguages() { + assertTrue(util.getLanguages().size() >= 1); + assertTrue(util.getLanguages().contains("JavaScript")); + } + + @Test + public void testIsTrustedScript() { + util.setTrustedScript("JavaScript", "foobar", true); + assertTrue(util.isTrustedScript("JavaScript", "foobar")); + assertTrue(util.isTrustedScript("JavaScript", " \n foobar \n\t\r")); + assertFalse(util.isTrustedScript("JavaScript", "foo\nbar")); + assertFalse(util.isTrustedScript("Javascript", "foobar")); + + // Empty script is always considered trusted + assertFalse(util.isTrustedScript("foo", null)); + assertTrue(util.isTrustedScript("foo", "")); + assertTrue(util.isTrustedScript("foo", " \n\r\t ")); + } + + @Test + public void testSetTrustedScript() { + util.setTrustedScript("JavaScript", " \n foobar \n\r ", true); + assertTrue(util.prefs.getNode(ScriptingUtil.NODE_ID).getBoolean(HASH_JavaScript_foobar, false)); + util.setTrustedScript("JavaScript", " foobar ", false); + assertTrue(util.prefs.getNode(ScriptingUtil.NODE_ID).getBoolean(HASH_JavaScript_foobar, true)); + assertFalse(util.prefs.getNode(ScriptingUtil.NODE_ID).getBoolean(HASH_JavaScript_foobar, false)); + } + + @Test + public void testClearTrustedScripts() { + util.setTrustedScript("JavaScript", "foobar", true); + assertTrue(util.isTrustedScript("JavaScript", "foobar")); + util.clearTrustedScripts(); + assertFalse(util.isTrustedScript("JavaScript", "foobar")); + } + + @Test + public void testNormalize() { + assertEquals("foo", ScriptingUtil.normalize("foo")); + assertEquals("foo bar", ScriptingUtil.normalize(" \n\r\t foo \r bar \n\t\r ")); + } + + @Test + public void testHash() { + assertEquals("SHA-256:12e6a78889b96a16d305b8e4af81119545f89eccba5fb37cc3a1ec2c53eab514", ScriptingUtil.hash("JS", "")); + assertEquals("SHA-256:000753e5deb2d8fa80e602ca03bcdb8e12a6b14b2b4a4d0abecdc976ad26e3ef", ScriptingUtil.hash("foo", "1165")); + assertEquals(HASH_JavaScript_foobar, ScriptingUtil.hash("JavaScript", "foobar")); + } +} diff --git a/core/test/net/sf/openrocket/startup/MockPreferences.java b/core/test/net/sf/openrocket/startup/MockPreferences.java new file mode 100644 index 000000000..eabfe18d9 --- /dev/null +++ b/core/test/net/sf/openrocket/startup/MockPreferences.java @@ -0,0 +1,108 @@ +package net.sf.openrocket.startup; + +import java.util.Set; +import java.util.prefs.BackingStoreException; + +import net.sf.openrocket.material.Material; +import net.sf.openrocket.preset.ComponentPreset; +import net.sf.openrocket.preset.ComponentPreset.Type; +import net.sf.openrocket.util.BugException; + +public class MockPreferences extends Preferences { + + private final String NODENAME = "OpenRocket-test-mock"; + private final java.util.prefs.Preferences NODE; + + public MockPreferences() { + java.util.prefs.Preferences root = java.util.prefs.Preferences.userRoot(); + try { + if (root.nodeExists(NODENAME)) { + root.node(NODENAME).removeNode(); + } + } catch (BackingStoreException e) { + throw new BugException("Unable to clear preference node", e); + } + NODE = root.node(NODENAME); + } + + @Override + public boolean getBoolean(String key, boolean def) { + return NODE.getBoolean(key, def); + } + + @Override + public void putBoolean(String key, boolean value) { + NODE.putBoolean(key, value); + } + + @Override + public int getInt(String key, int def) { + return NODE.getInt(key, def); + } + + @Override + public void putInt(String key, int value) { + NODE.putInt(key, value); + } + + @Override + public double getDouble(String key, double def) { + return NODE.getDouble(key, def); + } + + @Override + public void putDouble(String key, double value) { + NODE.putDouble(key, value); + } + + @Override + public String getString(String key, String def) { + return NODE.get(key, def); + } + + @Override + public void putString(String key, String value) { + NODE.put(key, value); + } + + @Override + public String getString(String directory, String key, String def) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public void putString(String directory, String key, String value) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public java.util.prefs.Preferences getNode(String nodeName) { + return NODE.node(nodeName); + } + + @Override + public void addUserMaterial(Material m) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public Set getUserMaterials() { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public void removeUserMaterial(Material m) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public void setComponentFavorite(ComponentPreset preset, Type type, boolean favorite) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public Set getComponentFavorites(Type type) { + throw new UnsupportedOperationException("Not yet implemented"); + } + +} 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..8305c734c 100644 --- a/swing/.classpath +++ b/swing/.classpath @@ -1,25 +1,26 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/swing/build.xml b/swing/build.xml index 6b01804e6..d6c4f9d10 100644 --- a/swing/build.xml +++ b/swing/build.xml @@ -105,6 +105,7 @@ + diff --git a/swing/lib/rsyntaxtextarea-2.5.6.jar b/swing/lib/rsyntaxtextarea-2.5.6.jar new file mode 100644 index 000000000..b60def66e Binary files /dev/null and b/swing/lib/rsyntaxtextarea-2.5.6.jar differ diff --git a/swing/resources/datafiles/examples/Simulation extensions and scripting.ork b/swing/resources/datafiles/examples/Simulation extensions and scripting.ork new file mode 100644 index 000000000..a1592fc11 Binary files /dev/null and b/swing/resources/datafiles/examples/Simulation extensions and scripting.ork differ diff --git a/swing/resources/datafiles/examples/Simulation listeners.ork b/swing/resources/datafiles/examples/Simulation listeners.ork deleted file mode 100644 index d4fbae528..000000000 Binary files a/swing/resources/datafiles/examples/Simulation listeners.ork and /dev/null differ diff --git a/swing/src/net/sf/openrocket/gui/simulation/SimulationEditDialog.java b/swing/src/net/sf/openrocket/gui/simulation/SimulationEditDialog.java index 53468bb61..7f8307225 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()); + } } } } @@ -167,7 +170,7 @@ public class SimulationEditDialog extends JDialog { //// Launch conditions tabbedPane.addTab(trans.get("simedtdlg.tab.Launchcond"), new SimulationConditionsPanel(simulation[0])); //// Simulation options - tabbedPane.addTab(trans.get("simedtdlg.tab.Simopt"), new SimulationOptionsPanel(simulation[0])); + tabbedPane.addTab(trans.get("simedtdlg.tab.Simopt"), new SimulationOptionsPanel(document, simulation[0])); tabbedPane.setSelectedIndex(0); diff --git a/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java b/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java index b94748b0f..6a925edef 100644 --- a/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java +++ b/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java @@ -1,54 +1,70 @@ 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; +import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.document.Simulation; import net.sf.openrocket.gui.SpinnerEditor; 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.startup.Preferences; 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(); - + + private OpenRocketDocument document; final Simulation simulation; - - SimulationOptionsPanel(final Simulation simulation) { + + private JPanel currentExtensions; + + SimulationOptionsPanel(OpenRocketDocument document, final Simulation simulation) { super(new MigLayout("fill")); + this.document = document; this.simulation = simulation; - + final SimulationOptions conditions = simulation.getOptions(); - + JPanel sub, subsub; String tip; JLabel label; @@ -56,7 +72,7 @@ class SimulationOptionsPanel extends JPanel { JSpinner spin; UnitSelector unit; BasicSlider slider; - + // // Simulation options sub = new JPanel(new MigLayout("fill, gap rel unrel", "[grow][65lp!][30lp!][75lp!]", "")); @@ -64,38 +80,38 @@ class SimulationOptionsPanel extends JPanel { sub.setBorder(BorderFactory.createTitledBorder(trans .get("simedtdlg.border.Simopt"))); this.add(sub, "growx, growy, aligny 0"); - + // Separate panel for computation methods, as they use a different // layout subsub = new JPanel(new MigLayout("insets 0, fill", "[grow][min!][min!][]")); - + // // Calculation method: tip = trans.get("simedtdlg.lbl.ttip.Calcmethod"); label = new JLabel(trans.get("simedtdlg.lbl.Calcmethod")); label.setToolTipText(tip); subsub.add(label, "gapright para"); - + // // Extended Barrowman label = new JLabel(trans.get("simedtdlg.lbl.ExtBarrowman")); label.setToolTipText(tip); subsub.add(label, "growx, span 3, wrap"); - + // Simulation method tip = trans.get("simedtdlg.lbl.ttip.Simmethod1") + trans.get("simedtdlg.lbl.ttip.Simmethod2"); label = new JLabel(trans.get("simedtdlg.lbl.Simmethod")); label.setToolTipText(tip); subsub.add(label, "gapright para"); - + label = new JLabel("6-DOF Runge-Kutta 4"); label.setToolTipText(tip); subsub.add(label, "growx, span 3, wrap"); - + // // Geodetic calculation method: label = new JLabel(trans.get("simedtdlg.lbl.GeodeticMethod")); label.setToolTipText(trans.get("simedtdlg.lbl.ttip.GeodeticMethodTip")); subsub.add(label, "gapright para"); - + EnumModel gcsModel = new EnumModel( conditions, "GeodeticComputation"); final JComboBox gcsCombo = new JComboBox(gcsModel); @@ -110,7 +126,7 @@ class SimulationOptionsPanel extends JPanel { gcsCombo.addActionListener(gcsTTipListener); gcsTTipListener.actionPerformed(null); subsub.add(gcsCombo, "span 3, wrap para"); - + // // Time step: label = new JLabel(trans.get("simedtdlg.lbl.Timestep")); @@ -121,25 +137,25 @@ class SimulationOptionsPanel extends JPanel { .toStringUnit(RK4SimulationStepper.RECOMMENDED_TIME_STEP) + "."; label.setToolTipText(tip); - subsub.add(label,"gapright para"); - + subsub.add(label, "gapright para"); + m = new DoubleModel(conditions, "TimeStep", UnitGroup.UNITS_TIME_STEP, 0, 1); - + spin = new JSpinner(m.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); spin.setToolTipText(tip); subsub.add(spin, ""); - + unit = new UnitSelector(m); unit.setToolTipText(tip); subsub.add(unit, ""); slider = new BasicSlider(m.getSliderModel(0, 0.2)); slider.setToolTipText(tip); subsub.add(slider, "w 100"); - + sub.add(subsub, "spanx, wrap para"); - + // Reset to default button JButton button = new JButton(trans.get("simedtdlg.but.resettodefault")); // Reset the time step to its default value ( @@ -159,141 +175,227 @@ class SimulationOptionsPanel extends JPanel { GeodeticComputationStrategy.SPHERICAL)); } }); - + sub.add(button, "align left"); - - // 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"); - - // // 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(); + desc.setText(trans.get("simedtdlg.SimExt.desc")); + sub.add(desc, "aligny 0, hmin 100lp, growx, wrap para"); + + + final JButton addExtension = new JButton(trans.get("simedtdlg.SimExt.add")); + 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"); - - // // 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]); + sub.add(addExtension, "growx, wrap 0"); + + 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(); + SwingSimulationExtensionConfigurator configurator = findConfigurator(e); + if (configurator != null) { + configurator.configure(e, simulation, SwingUtilities.windowForComponent(SimulationOptionsPanel.this)); + } + } + }); + menu.add(item); } - listenerModel.fireContentsChanged(); } + } + + JMenu copyMenu = null; + for (Simulation sim : document.getSimulations()) { + if (!sim.getSimulationExtensions().isEmpty()) { + JMenu menu = new JMenu(sim.getName()); + for (final SimulationExtension ext : sim.getSimulationExtensions()) { + JMenuItem item = new JMenuItem(ext.getName()); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + SimulationExtension e = ext.clone(); + simulation.getSimulationExtensions().add(e); + updateCurrentExtensions(); + SwingSimulationExtensionConfigurator configurator = findConfigurator(e); + if (configurator != null) { + configurator.configure(e, simulation, SwingUtilities.windowForComponent(SimulationOptionsPanel.this)); + } + } + }); + menu.add(item); + } + + if (copyMenu == null) { + copyMenu = new JMenu(trans.get("simedtdlg.SimExt.copyExtension")); + } + copyMenu.add(menu); + } + } + if (copyMenu != null) { + basemenu.add(copyMenu); + } + + return basemenu; + } + + 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(); + + 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 (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"); + } + + 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>() { }); - sub.add(button, "sizegroup buttons, alignx 50%"); - - } - - private class ListenerCellRenderer extends JLabel implements - ListCellRenderer { - - @Override - public Component getListCellRendererComponent(JList list, Object value, - int index, boolean isSelected, boolean cellHasFocus) { - 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; + for (SwingSimulationExtensionConfigurator c : configurators) { + if (c.support(extension)) { + return c; } - - 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; } + 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()); - } - } - + } 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 ff67daefa..48f1ccf0e 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); @@ -78,6 +78,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/gui/util/SwingPreferences.java b/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java index 499620c9b..b4bd15cdf 100644 --- a/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java +++ b/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java @@ -45,8 +45,8 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences { for (String lang : new String[] { "en", "de", "es", "fr", "it", "ru", "cs", "pl", "ja", "pt", "tr" }) { list.add(new Locale(lang)); } - list.add(new Locale("zh","CN")); - list.add(new Locale("uk","UA")); + list.add(new Locale("zh", "CN")); + list.add(new Locale("uk", "UA")); SUPPORTED_LOCALES = Collections.unmodifiableList(list); } @@ -200,6 +200,7 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences { * @param nodeName the node name * @return the preferences object for that node */ + @Override public Preferences getNode(String nodeName) { return PREFNODE.node(nodeName); } @@ -418,11 +419,11 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences { public boolean computeFlightInBackground() { return PREFNODE.getBoolean("backgroundFlight", true); } - + public void setComputeFlightInBackground(boolean b) { PREFNODE.putBoolean("backgroundFlight", b); } - + public Simulation getBackgroundSimulation(Rocket rocket) { Simulation s = new Simulation(rocket); SimulationOptions cond = s.getOptions(); 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..8eb2ea1ae --- /dev/null +++ b/swing/src/net/sf/openrocket/simulation/extension/AbstractSwingSimulationExtensionConfigurator.java @@ -0,0 +1,88 @@ +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; + + private JDialog dialog; + + 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) { + 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); + close(); + GUIUtil.setNullModels(dialog); + dialog = null; + } + + /** + * Return a title for the dialog window. By default uses the extension's name. + */ + protected String getTitle(SimulationExtension extension, Simulation simulation) { + return extension.getName(); + } + + /** + * Return the dialog currently open. + */ + protected JDialog getDialog() { + return dialog; + } + + /** + * Called when the default dialog is closed. By default does nothing. + */ + protected void close() { + + } + + 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..8b7879526 --- /dev/null +++ b/swing/src/net/sf/openrocket/simulation/extension/impl/AirStartConfigurator.java @@ -0,0 +1,58 @@ +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"); + + + panel.add(new JLabel("Launch velocity:")); + + m = new DoubleModel(extension, "LaunchVelocity", UnitGroup.UNITS_VELOCITY, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "w 65lp!"); + + unit = new UnitSelector(m); + panel.add(unit, "w 25"); + + slider = new BasicSlider(m.getSliderModel(0, 150)); + 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; + } + +} diff --git a/swing/src/net/sf/openrocket/simulation/extension/impl/ScriptingConfigurator.java b/swing/src/net/sf/openrocket/simulation/extension/impl/ScriptingConfigurator.java new file mode 100644 index 000000000..1005e600e --- /dev/null +++ b/swing/src/net/sf/openrocket/simulation/extension/impl/ScriptingConfigurator.java @@ -0,0 +1,166 @@ +package net.sf.openrocket.simulation.extension.impl; + +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.util.Set; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JOptionPane; +import javax.swing.JPanel; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.gui.adaptors.BooleanModel; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.components.StyledLabel.Style; +import net.sf.openrocket.plugin.Plugin; +import net.sf.openrocket.simulation.extension.AbstractSwingSimulationExtensionConfigurator; + +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.fife.ui.rsyntaxtextarea.SyntaxConstants; +import org.fife.ui.rsyntaxtextarea.TokenMakerFactory; +import org.fife.ui.rtextarea.RTextScrollPane; + +import com.google.inject.Inject; + +@Plugin +public class ScriptingConfigurator extends AbstractSwingSimulationExtensionConfigurator { + + @Inject + private ScriptingUtil util; + + private JComboBox languageSelector; + private RSyntaxTextArea text; + private JCheckBox trusted; + + private ScriptingExtension extension; + private Simulation simulation; + + public ScriptingConfigurator() { + super(ScriptingExtension.class); + } + + @Override + protected JComponent getConfigurationComponent(final ScriptingExtension extension, Simulation simulation, JPanel panel) { + this.extension = extension; + this.simulation = simulation; + + panel.add(new StyledLabel(trans.get("SimulationExtension.scripting.language.label"), Style.BOLD), "spanx, split"); + + String[] languages = util.getLanguages().toArray(new String[0]); + languageSelector = new JComboBox(languages); + languageSelector.setEditable(false); + languageSelector.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + setLanguage((String) languageSelector.getSelectedItem()); + } + }); + panel.add(languageSelector, "wrap para"); + + + text = new RSyntaxTextArea(extension.getScript(), 20, 80); + text.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT); + text.setCodeFoldingEnabled(true); + text.setLineWrap(true); + text.setWrapStyleWord(true); + text.setEditable(true); + text.setCurrentLineHighlightColor(new Color(255, 255, 230)); + text.addFocusListener(new FocusListener() { + @Override + public void focusGained(FocusEvent event) { + } + + @Override + public void focusLost(FocusEvent event) { + String str = text.getText(); + if (!extension.getScript().equals(str)) { + extension.setScript(str); + } + } + }); + + RTextScrollPane scroll = new RTextScrollPane(text); + panel.add(scroll, "spanx, grow, wrap para"); + + + BooleanModel enabled = new BooleanModel(extension, "Enabled"); + JCheckBox check = new JCheckBox(enabled); + check.setText(trans.get("SimulationExtension.scripting.text.enabled")); + check.setToolTipText(trans.get("SimulationExtension.scripting.text.enabled.ttip")); + panel.add(check, "spanx, wrap rel"); + + trusted = new JCheckBox(trans.get("SimulationExtension.scripting.text.trusted")); + trusted.setSelected(util.isTrustedScript(extension.getLanguage(), extension.getScript())); + panel.add(trusted, "spanx, split"); + + panel.add(new JPanel(), "growx"); + + JButton button = new JButton(trans.get("SimulationExtension.scripting.text.trusted.clear")); + button.setToolTipText(trans.get("SimulationExtension.scripting.text.trusted.clear.ttip")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + util.clearTrustedScripts(); + JOptionPane.showMessageDialog(getDialog(), trans.get("SimulationExtension.scripting.text.trusted.cleared"), + trans.get("SimulationExtension.scripting.text.trusted.cleared.title"), JOptionPane.INFORMATION_MESSAGE); + } + }); + panel.add(button, "wrap rel"); + + + StyledLabel label = new StyledLabel(trans.get("SimulationExtension.scripting.text.trusted.msg"), -1, Style.ITALIC); + panel.add(label); + + setLanguage(util.getLanguage(extension.getLanguage())); + + return panel; + } + + @Override + protected void close() { + util.setTrustedScript(extension.getLanguage(), extension.getScript(), trusted.isSelected()); + } + + + private void setLanguage(String language) { + if (language == null) { + language = ""; + } + if (!language.equals(languageSelector.getSelectedItem())) { + languageSelector.setSelectedItem(language); + } + extension.setLanguage(language); + text.setSyntaxEditingStyle(findSyntaxLanguage(language)); + getDialog().setTitle(getTitle(extension, simulation)); + } + + private String findSyntaxLanguage(String language) { + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName(language); + + if (engine != null) { + Set supported = TokenMakerFactory.getDefaultInstance().keySet(); + for (String type : engine.getFactory().getMimeTypes()) { + if (supported.contains(type)) { + return type; + } + for (String match : supported) { + if (match.contains("/" + language.toLowerCase())) { + return match; + } + } + } + } + + return SyntaxConstants.SYNTAX_STYLE_NONE; + } + +} diff --git a/swing/src/net/sf/openrocket/utils/Scripting.java b/swing/src/net/sf/openrocket/utils/Scripting.java new file mode 100644 index 000000000..ca1a2993d --- /dev/null +++ b/swing/src/net/sf/openrocket/utils/Scripting.java @@ -0,0 +1,38 @@ +package net.sf.openrocket.utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.script.ScriptEngineFactory; +import javax.script.ScriptEngineManager; + +import org.fife.ui.rsyntaxtextarea.TokenMakerFactory; + +public class Scripting { + + public static void main(String[] args) { + System.out.println("Scripting APIs:"); + + ScriptEngineManager manager = new ScriptEngineManager(); + for (ScriptEngineFactory factory : manager.getEngineFactories()) { + System.out.println(" engineName=" + factory.getEngineName() + + " engineVersion=" + factory.getEngineVersion() + + " languageName=" + factory.getLanguageName() + + " languageVersion=" + factory.getLanguageVersion() + + " names=" + factory.getNames() + + " mimeTypes=" + factory.getMimeTypes() + + " extensions=" + factory.getExtensions()); + } + System.out.println(); + + System.out.println("RSyntaxTextArea supported syntax languages:"); + TokenMakerFactory f = TokenMakerFactory.getDefaultInstance(); + List list = new ArrayList(f.keySet()); + Collections.sort(list); + for (String type : list) { + System.out.println(" " + type); + } + System.out.println(); + } +}
+ * By default, this method does nothing. + */ + @Override + public void documentLoaded(OpenRocketDocument document, Simulation simulation, WarningSet warnings) { + + } + + /** + * By default, returns a new object obtained by calling Object.clone() and + * cloning the config object. + */ + @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 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 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..f0b6c93c2 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/SimulationExtension.java @@ -0,0 +1,94 @@ +package net.sf.openrocket.simulation.extension; + +import java.util.List; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.Simulation; +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(); + + /** + * Called once for each simulation this extension is attached to when loading a document. + * This may perform necessary changes to the document at load time. + * + * @param document the loaded document + * @param simulation the simulation this extension is attached to + * @param warnings the document loading warnings + */ + public void documentLoaded(OpenRocketDocument document, Simulation simulation, WarningSet warnings); + + /** + * 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..e591685b3 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/AirStart.java @@ -0,0 +1,58 @@ +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; + if (getLaunchVelocity() > 0.01) { + name = trans.get("SimulationExtension.airstart.name.altvel"); + } else { + name = trans.get("SimulationExtension.airstart.name.alt"); + } + name = L10N.replace(name, "{alt}", UnitGroup.UNITS_DISTANCE.toStringUnit(getLaunchAltitude())); + name = L10N.replace(name, "{vel}", UnitGroup.UNITS_VELOCITY.toStringUnit(getLaunchVelocity())); + return name; + } + + public double getLaunchAltitude() { + return config.getDouble("launchAltitude", 0.0); + } + + public void setLaunchAltitude(double launchAltitude) { + config.put("launchAltitude", launchAltitude); + fireChangeEvent(); + } + + public double getLaunchVelocity() { + return config.getDouble("launchVelocity", 0.0); + } + + public void setLaunchVelocity(double launchVelocity) { + config.put("launchVelocity", launchVelocity); + fireChangeEvent(); + } + + + private class AirStartListener extends AbstractSimulationListener { + @Override + public void startSimulation(SimulationStatus status) throws SimulationException { + status.setRocketPosition(new Coordinate(0, 0, getLaunchAltitude())); + status.setRocketVelocity(status.getRocketOrientationQuaternion().rotate(new Coordinate(0, 0, getLaunchVelocity()))); + } + } +} 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/simulation/extension/impl/ScriptingExtension.java b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingExtension.java new file mode 100644 index 000000000..3569d2aae --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingExtension.java @@ -0,0 +1,112 @@ +package net.sf.openrocket.simulation.extension.impl; + +import javax.script.Invocable; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.l10n.L10N; +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 com.google.inject.Inject; + +public class ScriptingExtension extends AbstractSimulationExtension { + + private static final String DEFAULT_LANGUAGE = "JavaScript"; + + @Inject + private ScriptingUtil util; + + + public ScriptingExtension() { + setLanguage(DEFAULT_LANGUAGE); + setScript(""); + setEnabled(true); + } + + @Override + public String getName() { + String name = trans.get("SimulationExtension.scripting.name"); + name = L10N.replace(name, "{language}", getLanguage()); + return name; + } + + @Override + public String getDescription() { + return trans.get("SimulationExtension.scripting.desc"); + } + + @Override + public void documentLoaded(OpenRocketDocument document, Simulation simulation, WarningSet warnings) { + /* + * Scripts that the user has not explicitly indicated as trusted are disabled + * when loading from a file. This is to prevent trojans. + */ + if (isEnabled()) { + if (!util.isTrustedScript(getLanguage(), getScript())) { + setEnabled(false); + warnings.add(Warning.fromString(trans.get("SimulationExtension.scripting.warning.disabled"))); + } + } + } + + @Override + public void initialize(SimulationConditions conditions) throws SimulationException { + if (isEnabled()) { + conditions.getSimulationListenerList().add(getListener()); + } + } + + + public String getScript() { + return config.getString("script", ""); + } + + public void setScript(String script) { + config.put("script", script); + } + + public String getLanguage() { + return config.getString("language", DEFAULT_LANGUAGE); + } + + public void setLanguage(String language) { + config.put("language", language); + } + + public boolean isEnabled() { + return config.getBoolean("enabled", false); + } + + public void setEnabled(boolean enabled) { + config.put("enabled", enabled); + } + + + SimulationListener getListener() throws SimulationException { + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName(getLanguage()); + if (engine == null) { + throw new SimulationException("Your JRE does not support the scripting language '" + getLanguage() + "'"); + } + + try { + engine.eval(getScript()); + } catch (ScriptException e) { + throw new SimulationException("Invalid script: " + e.getMessage()); + } + + if (!(engine instanceof Invocable)) { + throw new SimulationException("The scripting language '" + getLanguage() + "' does not implement the Invocable interface"); + } + return new ScriptingSimulationListener((Invocable) engine); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingProvider.java b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingProvider.java new file mode 100644 index 000000000..911b47214 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingProvider.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 ScriptingProvider extends AbstractSimulationExtensionProvider { + + public ScriptingProvider() { + super(ScriptingExtension.class, "User code", "Scripts"); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingSimulationListener.java b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingSimulationListener.java new file mode 100644 index 000000000..5eab2dfa8 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingSimulationListener.java @@ -0,0 +1,227 @@ +package net.sf.openrocket.simulation.extension.impl; + +import java.util.HashSet; +import java.util.Set; + +import javax.script.Invocable; +import javax.script.ScriptException; + +import net.sf.openrocket.aerodynamics.AerodynamicForces; +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.models.atmosphere.AtmosphericConditions; +import net.sf.openrocket.motor.MotorId; +import net.sf.openrocket.motor.MotorInstance; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.rocketcomponent.RecoveryDevice; +import net.sf.openrocket.simulation.AccelerationData; +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.simulation.MassData; +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.simulation.exception.SimulationListenerException; +import net.sf.openrocket.simulation.listeners.SimulationComputationListener; +import net.sf.openrocket.simulation.listeners.SimulationEventListener; +import net.sf.openrocket.simulation.listeners.SimulationListener; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Coordinate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ScriptingSimulationListener implements SimulationListener, SimulationComputationListener, SimulationEventListener, Cloneable { + + private final static Logger logger = LoggerFactory.getLogger(ScriptingSimulationListener.class); + + /* + * NOTE: This class is used instead of using the scripting interface API + * so that unimplemented script methods are not called unnecessarily. + */ + + private Invocable invocable; + private Set missing = new HashSet(); + + + public ScriptingSimulationListener(Invocable invocable) { + this.invocable = invocable; + } + + + @Override + public boolean isSystemListener() { + return false; + } + + + @Override + public SimulationListener clone() { + try { + ScriptingSimulationListener clone = (ScriptingSimulationListener) super.clone(); + clone.missing = new HashSet(missing); + return clone; + } catch (CloneNotSupportedException e) { + throw new BugException(e); + } + } + + + + //// SimulationListener //// + + @Override + public void startSimulation(SimulationStatus status) throws SimulationException { + invoke(Void.class, null, "startSimulation", status); + } + + @Override + public void endSimulation(SimulationStatus status, SimulationException exception) { + try { + invoke(Void.class, null, "endSimulation", status, exception); + } catch (SimulationException e) { + } + } + + @Override + public boolean preStep(SimulationStatus status) throws SimulationException { + return invoke(Boolean.class, true, "preStep", status); + } + + @Override + public void postStep(SimulationStatus status) throws SimulationException { + invoke(Void.class, null, "postStep", status); + } + + + + //// SimulationEventListener //// + + @Override + public boolean addFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { + return invoke(Boolean.class, true, "addFlightEvent", status, event); + } + + @Override + public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { + return invoke(Boolean.class, true, "handleFlightEvent", status, event); + } + + @Override + public boolean motorIgnition(SimulationStatus status, MotorId motorId, MotorMount mount, MotorInstance instance) throws SimulationException { + return invoke(Boolean.class, true, "motorIgnition", status, motorId, mount, instance); + } + + @Override + public boolean recoveryDeviceDeployment(SimulationStatus status, RecoveryDevice recoveryDevice) throws SimulationException { + return invoke(Boolean.class, true, "recoveryDeviceDeployment", status, recoveryDevice); + } + + + + //// SimulationComputationListener //// + + @Override + public AccelerationData preAccelerationCalculation(SimulationStatus status) throws SimulationException { + return invoke(AccelerationData.class, null, "preAccelerationCalculation", status); + } + + @Override + public AerodynamicForces preAerodynamicCalculation(SimulationStatus status) throws SimulationException { + return invoke(AerodynamicForces.class, null, "preAerodynamicCalculation", status); + } + + @Override + public AtmosphericConditions preAtmosphericModel(SimulationStatus status) throws SimulationException { + return invoke(AtmosphericConditions.class, null, "preAtmosphericModel", status); + } + + @Override + public FlightConditions preFlightConditions(SimulationStatus status) throws SimulationException { + return invoke(FlightConditions.class, null, "preFlightConditions", status); + } + + @Override + public double preGravityModel(SimulationStatus status) throws SimulationException { + return invoke(Double.class, Double.NaN, "preGravityModel", status); + } + + @Override + public MassData preMassCalculation(SimulationStatus status) throws SimulationException { + return invoke(MassData.class, null, "preMassCalculation", status); + } + + @Override + public double preSimpleThrustCalculation(SimulationStatus status) throws SimulationException { + return invoke(Double.class, Double.NaN, "preSimpleThrustCalculation", status); + } + + @Override + public Coordinate preWindModel(SimulationStatus status) throws SimulationException { + return invoke(Coordinate.class, null, "preWindModel", status); + } + + @Override + public AccelerationData postAccelerationCalculation(SimulationStatus status, AccelerationData acceleration) throws SimulationException { + return invoke(AccelerationData.class, null, "postAccelerationCalculation", status, acceleration); + } + + @Override + public AerodynamicForces postAerodynamicCalculation(SimulationStatus status, AerodynamicForces forces) throws SimulationException { + return invoke(AerodynamicForces.class, null, "postAerodynamicCalculation", status, forces); + } + + @Override + public AtmosphericConditions postAtmosphericModel(SimulationStatus status, AtmosphericConditions atmosphericConditions) throws SimulationException { + return invoke(AtmosphericConditions.class, null, "postAtmosphericModel", status, atmosphericConditions); + } + + @Override + public FlightConditions postFlightConditions(SimulationStatus status, FlightConditions flightConditions) throws SimulationException { + return invoke(FlightConditions.class, null, "postFlightConditions", status, flightConditions); + } + + @Override + public double postGravityModel(SimulationStatus status, double gravity) throws SimulationException { + return invoke(Double.class, Double.NaN, "postGravityModel", status, gravity); + } + + @Override + public MassData postMassCalculation(SimulationStatus status, MassData massData) throws SimulationException { + return invoke(MassData.class, null, "postMassCalculation", status, massData); + } + + @Override + public double postSimpleThrustCalculation(SimulationStatus status, double thrust) throws SimulationException { + return invoke(Double.class, Double.NaN, "postSimpleThrustCalculation", status, thrust); + } + + @Override + public Coordinate postWindModel(SimulationStatus status, Coordinate wind) throws SimulationException { + return invoke(Coordinate.class, null, "postWindModel", status, wind); + } + + + @SuppressWarnings("unchecked") + private T invoke(Class retType, T def, String method, Object... args) throws SimulationException { + try { + if (!missing.contains(method)) { + Object o = invocable.invokeFunction(method, args); + if (o == null) { + // Use default/null if function returns nothing + return def; + } else if (!o.getClass().equals(retType)) { + throw new SimulationListenerException("Custom script function " + method + " returned type " + + o.getClass().getSimpleName() + ", expected " + retType.getSimpleName()); + } else { + return (T) o; + } + } + } catch (NoSuchMethodException e) { + missing.add(method); + // fall-through + } catch (ScriptException e) { + logger.warn("Script exception in " + method + ": " + e, e); + throw new SimulationException("Script failed: " + e.getMessage()); + } + return def; + } + +} diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingUtil.java b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingUtil.java new file mode 100644 index 000000000..6f0f76517 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingUtil.java @@ -0,0 +1,158 @@ +package net.sf.openrocket.simulation.extension.impl; + +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.List; +import java.util.prefs.BackingStoreException; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; +import javax.script.ScriptEngineManager; + +import net.sf.openrocket.startup.Preferences; +import net.sf.openrocket.util.ArrayList; +import net.sf.openrocket.util.BugException; + +import com.google.inject.Inject; + +/** + * Utility class used by the scripting extension and its configurator. + */ +public class ScriptingUtil { + + static final String NODE_ID = ScriptingExtension.class.getCanonicalName(); + + private static final List DEFAULT_TRUSTED_HASHES = Arrays.asList( + // Roll control script in roll control example file: + "SHA-256:9bf364ce4d4a75f09b29178bf9d6872b232084f73dae20dc7b5b073e54e95a42" + ); + + /** The name to be chosen from a list of alternatives. If not found, will use the default name. */ + private static final List PREFERRED_LANGUAGE_NAMES = Arrays.asList("JavaScript"); + + @Inject + Preferences prefs; + + + + + /** + * Return the preferred internal language name based on a script language name. + * + * @return the preferred language name, or null if the language is not supported. + */ + public String getLanguage(String language) { + if (language == null) { + return null; + } + + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName(language); + if (engine == null) { + return null; + } + return getLanguage(engine.getFactory()); + } + + + public List getLanguages() { + List langs = new ArrayList(); + ScriptEngineManager manager = new ScriptEngineManager(); + for (ScriptEngineFactory factory : manager.getEngineFactories()) { + langs.add(getLanguage(factory)); + } + return langs; + } + + + private String getLanguage(ScriptEngineFactory factory) { + for (String name : factory.getNames()) { + if (PREFERRED_LANGUAGE_NAMES.contains(name)) { + return name; + } + } + + return factory.getLanguageName(); + } + + + + /** + * Test whether the user has indicated this script to be trusted, + * or if it is an internally trusted script. + */ + public boolean isTrustedScript(String language, String script) { + if (language == null || script == null) { + return false; + } + script = normalize(script); + if (script.length() == 0) { + return true; + } + String hash = hash(language, script); + if (DEFAULT_TRUSTED_HASHES.contains(hash)) { + return true; + } + return prefs.getNode(NODE_ID).getBoolean(hash, false); + } + + /** + * Mark a script as trusted. + */ + public void setTrustedScript(String language, String script, boolean trusted) { + script = normalize(script); + String hash = hash(language, script); + if (trusted) { + prefs.getNode(NODE_ID).putBoolean(hash, true); + } else { + prefs.getNode(NODE_ID).remove(hash); + } + } + + /** + * Clear all trusted scripts. + */ + public void clearTrustedScripts() { + try { + prefs.getNode(NODE_ID).clear(); + } catch (BackingStoreException e) { + throw new BugException(e); + } + } + + + static String normalize(String script) { + return script.replaceAll("\r", "").trim(); + } + + static String hash(String language, String script) { + /* + * NOTE: Hash length must be max 80 chars, the max length of a key in a Properties object. + */ + + String output; + MessageDigest digest; + + try { + digest = MessageDigest.getInstance("SHA-256"); + digest.update(language.getBytes("UTF-8")); + digest.update((byte) '|'); + byte[] hash = digest.digest(script.getBytes("UTF-8")); + BigInteger bigInt = new BigInteger(1, hash); + output = bigInt.toString(16); + while (output.length() < 64) { + output = "0" + output; + } + } catch (NoSuchAlgorithmException e) { + throw new BugException("JRE does not support SHA-256 hash algorithm", e); + } catch (UnsupportedEncodingException e) { + throw new BugException(e); + } + + return digest.getAlgorithm() + ":" + output; + } + +} diff --git a/core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java b/core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java index 0c8a3d32d..1ad3ef4ad 100644 --- a/core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java +++ b/core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java @@ -8,11 +8,11 @@ import net.sf.openrocket.motor.MotorInstance; import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.rocketcomponent.RecoveryDevice; import net.sf.openrocket.simulation.AccelerationData; -import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.FlightEvent; import net.sf.openrocket.simulation.MassData; import net.sf.openrocket.simulation.SimulationStatus; import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.Coordinate; @@ -24,20 +24,10 @@ import net.sf.openrocket.util.Coordinate; * @author Sampo Niskanen */ public class AbstractSimulationListener implements SimulationListener, SimulationComputationListener, - SimulationEventListener { + SimulationEventListener, Cloneable { //// SimulationListener //// - @Override - public String getName() { - return this.getClass().getSimpleName(); - } - - @Override - public String[] getMenuPosition() { - return new String[0]; - } - @Override public void startSimulation(SimulationStatus status) throws SimulationException { // No-op @@ -68,14 +58,6 @@ public class AbstractSimulationListener implements SimulationListener, Simulatio return false; } - /** - * Return an array of any flight data types this listener creates. - */ - @Override - public FlightDataType[] getFlightDataTypes() { - return new FlightDataType[] {}; - } - //// SimulationEventListener //// @@ -184,8 +166,12 @@ public class AbstractSimulationListener implements SimulationListener, Simulatio } @Override - public AbstractSimulationListener clone() throws CloneNotSupportedException { - return (AbstractSimulationListener) super.clone(); + public AbstractSimulationListener clone() { + try { + return (AbstractSimulationListener) super.clone(); + } catch (CloneNotSupportedException e) { + throw new BugException(e); + } } } diff --git a/core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java b/core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java index c02b25765..88422d72c 100644 --- a/core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java +++ b/core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java @@ -4,7 +4,6 @@ import net.sf.openrocket.aerodynamics.AerodynamicForces; import net.sf.openrocket.aerodynamics.FlightConditions; import net.sf.openrocket.models.atmosphere.AtmosphericConditions; import net.sf.openrocket.simulation.AccelerationData; -import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.MassData; import net.sf.openrocket.simulation.SimulationStatus; import net.sf.openrocket.simulation.exception.SimulationException; @@ -17,20 +16,20 @@ import net.sf.openrocket.util.Coordinate; * @author Sampo Niskanen */ public interface SimulationComputationListener extends SimulationListener { - - + + //////// Computation/modeling related callbacks //////// public AccelerationData preAccelerationCalculation(SimulationStatus status) throws SimulationException; public AccelerationData postAccelerationCalculation(SimulationStatus status, AccelerationData acceleration) - throws SimulationException; + throws SimulationException; public AtmosphericConditions preAtmosphericModel(SimulationStatus status) - throws SimulationException; + throws SimulationException; public AtmosphericConditions postAtmosphericModel(SimulationStatus status, AtmosphericConditions atmosphericConditions) - throws SimulationException; + throws SimulationException; public Coordinate preWindModel(SimulationStatus status) throws SimulationException; @@ -42,29 +41,27 @@ public interface SimulationComputationListener extends SimulationListener { public double postGravityModel(SimulationStatus status, double gravity) throws SimulationException; - + public FlightConditions preFlightConditions(SimulationStatus status) - throws SimulationException; + throws SimulationException; public FlightConditions postFlightConditions(SimulationStatus status, FlightConditions flightConditions) - throws SimulationException; + throws SimulationException; public AerodynamicForces preAerodynamicCalculation(SimulationStatus status) - throws SimulationException; + throws SimulationException; public AerodynamicForces postAerodynamicCalculation(SimulationStatus status, AerodynamicForces forces) - throws SimulationException; + throws SimulationException; public MassData preMassCalculation(SimulationStatus status) throws SimulationException; public MassData postMassCalculation(SimulationStatus status, MassData massData) throws SimulationException; - - + + public double preSimpleThrustCalculation(SimulationStatus status) throws SimulationException; public double postSimpleThrustCalculation(SimulationStatus status, double thrust) throws SimulationException; - - @Override - public FlightDataType[] getFlightDataTypes(); + } diff --git a/core/src/net/sf/openrocket/simulation/listeners/SimulationEventListener.java b/core/src/net/sf/openrocket/simulation/listeners/SimulationEventListener.java index 00ef6b7b5..300fb8645 100644 --- a/core/src/net/sf/openrocket/simulation/listeners/SimulationEventListener.java +++ b/core/src/net/sf/openrocket/simulation/listeners/SimulationEventListener.java @@ -4,14 +4,13 @@ import net.sf.openrocket.motor.MotorId; import net.sf.openrocket.motor.MotorInstance; import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.rocketcomponent.RecoveryDevice; -import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.FlightEvent; import net.sf.openrocket.simulation.SimulationStatus; import net.sf.openrocket.simulation.exception.SimulationException; public interface SimulationEventListener { - + /** * Called before adding a flight event to the event queue. * @@ -23,7 +22,7 @@ public interface SimulationEventListener { public boolean addFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException; - + /** * Called before handling a flight event. * @@ -57,10 +56,6 @@ public interface SimulationEventListener { */ public boolean recoveryDeviceDeployment(SimulationStatus status, RecoveryDevice recoveryDevice) throws SimulationException; - - - - public FlightDataType[] getFlightDataTypes(); - + } diff --git a/core/src/net/sf/openrocket/simulation/listeners/SimulationListener.java b/core/src/net/sf/openrocket/simulation/listeners/SimulationListener.java index bdbf90ffc..a0d4b147c 100644 --- a/core/src/net/sf/openrocket/simulation/listeners/SimulationListener.java +++ b/core/src/net/sf/openrocket/simulation/listeners/SimulationListener.java @@ -1,6 +1,5 @@ package net.sf.openrocket.simulation.listeners; -import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.SimulationStatus; import net.sf.openrocket.simulation.exception.SimulationException; @@ -10,26 +9,7 @@ import net.sf.openrocket.simulation.exception.SimulationException; * If the implementation maintains any state, it should be properly cloned. * */ -public interface SimulationListener extends Cloneable { - - /** - * Get the name of this simulation listener. Ideally this should be localized, as - * it can be displayed in the UI. - * - * @return the name of this simulation listener. - */ - public String getName(); - - - /** - * Get the menu position of this simulation listener. This should be an array - * of localized submenu names in descending order, or an empty array for positioning - * in the base menu. - * - * @return the menu position of this simulation listener. - */ - public String[] getMenuPosition(); - +public interface SimulationListener { /** * Called when starting a simulation. @@ -83,9 +63,7 @@ public interface SimulationListener extends Cloneable { /** - * Return a list of any flight data types this listener creates. + * Return a deep copy of this simulation listener including its state. */ - public FlightDataType[] getFlightDataTypes(); - - public SimulationListener clone() throws CloneNotSupportedException; + public SimulationListener clone(); } diff --git a/core/src/net/sf/openrocket/simulation/listeners/example/DampingMoment.java b/core/src/net/sf/openrocket/simulation/listeners/example/DampingMoment.java index 4cc5e9ed7..3e58beb5e 100644 --- a/core/src/net/sf/openrocket/simulation/listeners/example/DampingMoment.java +++ b/core/src/net/sf/openrocket/simulation/listeners/example/DampingMoment.java @@ -20,20 +20,7 @@ import net.sf.openrocket.util.Coordinate; public class DampingMoment extends AbstractSimulationListener { private static final FlightDataType type = FlightDataType.getType("Damping moment coefficient", "Cdm", UnitGroup.UNITS_COEFFICIENT); - private static final FlightDataType[] typeList = {type}; - - @Override - public String getName(){ - return "Damping moment listener"; - } - - /** - * Return a list of any flight data types this listener creates. - */ - @Override - public FlightDataType[] getFlightDataTypes(){ - return typeList; - } + private static final FlightDataType[] typeList = { type }; @Override public FlightConditions postFlightConditions(SimulationStatus status, FlightConditions flightConditions) throws SimulationException { @@ -42,11 +29,11 @@ public class DampingMoment extends AbstractSimulationListener { //status.getFlightData().setValue(type, aerodynamicPart + propulsivePart); status.getFlightData().setValue(type, calculate(status, flightConditions)); - + return flightConditions; } - private double calculate(SimulationStatus status, FlightConditions flightConditions){ + private double calculate(SimulationStatus status, FlightConditions flightConditions) { // Work out the propulsive/jet damping part of the moment. @@ -55,15 +42,15 @@ public class DampingMoment extends AbstractSimulationListener { List mpAll = data.get(FlightDataType.TYPE_PROPELLANT_MASS); List time = data.get(FlightDataType.TYPE_TIME); - if (mpAll == null || time == null){ + if (mpAll == null || time == null) { return Double.NaN; } int len = mpAll.size(); // This isn't as accurate as I would like - double mdot=Double.NaN; - if (len > 2){ + double mdot = Double.NaN; + if (len > 2) { // Using polynomial interpolator for derivative. Doesn't help much //double[] x = { time.get(len-5), time.get(len-4), time.get(len-3), time.get(len-2), time.get(len-1) }; //double[] y = { mpAll.get(len-5), mpAll.get(len-4), mpAll.get(len-3), mpAll.get(len-2), mpAll.get(len-1) }; @@ -71,22 +58,22 @@ public class DampingMoment extends AbstractSimulationListener { //double[] coeff = interp.interpolator(y); //double dt = .01; //mdot = (interp.eval(x[4], coeff) - interp.eval(x[4]-dt, coeff))/dt; - - mdot = (mpAll.get(len-1) - mpAll.get(len-2)) / (time.get(len-1) - time.get(len-2)); + + mdot = (mpAll.get(len - 1) - mpAll.get(len - 2)) / (time.get(len - 1) - time.get(len - 2)); } double cg = data.getLast(FlightDataType.TYPE_CG_LOCATION); // find the maximum distance from nose to nozzle. double nozzleDistance = 0; - for (MotorId id: status.getMotorConfiguration().getMotorIDs()){ + for (MotorId id : status.getMotorConfiguration().getMotorIDs()) { MotorInstanceConfiguration config = status.getMotorConfiguration(); Coordinate position = config.getMotorPosition(id); double x = position.x + config.getMotorInstance(id).getParentMotor().getLength(); - if (x > nozzleDistance){ + if (x > nozzleDistance) { nozzleDistance = x; - } + } } // now can get the propulsive part @@ -99,11 +86,12 @@ public class DampingMoment extends AbstractSimulationListener { // Must go through each component ... Map forces = aerocalc.getForceAnalysis(status.getConfiguration(), flightConditions, null); - for (Map.Entry entry : forces.entrySet()){ + for (Map.Entry entry : forces.entrySet()) { RocketComponent comp = entry.getKey(); - if (!comp.isAerodynamic()) continue; + if (!comp.isAerodynamic()) + continue; //System.out.println(comp.toString()); @@ -111,7 +99,7 @@ public class DampingMoment extends AbstractSimulationListener { double Cp = entry.getValue().getCP().length(); double z = comp.getPositionValue(); //? - aerodynamicPart += CNa*Math.pow(z-Cp, 2); + aerodynamicPart += CNa * Math.pow(z - Cp, 2); } double v = flightConditions.getVelocity(); @@ -121,7 +109,7 @@ public class DampingMoment extends AbstractSimulationListener { aerodynamicPart = aerodynamicPart * .5 * rho * v * ar; return aerodynamicPart + propulsivePart; - + } } diff --git a/core/src/net/sf/openrocket/startup/Application.java b/core/src/net/sf/openrocket/startup/Application.java index c9a5fdf82..13e8fdedf 100644 --- a/core/src/net/sf/openrocket/startup/Application.java +++ b/core/src/net/sf/openrocket/startup/Application.java @@ -4,6 +4,7 @@ import net.sf.openrocket.database.ComponentPresetDao; import net.sf.openrocket.database.motor.MotorDatabase; import net.sf.openrocket.database.motor.ThrustCurveMotorSetDatabase; import net.sf.openrocket.l10n.ClassBasedTranslator; +import net.sf.openrocket.l10n.DebugTranslator; import net.sf.openrocket.l10n.ExceptionSuppressingTranslator; import net.sf.openrocket.l10n.Translator; @@ -33,6 +34,10 @@ public final class Application { } private static Translator getBaseTranslator() { + if (injector == null) { + // Occurs in some unit tests + return new DebugTranslator(null); + } return injector.getInstance(Translator.class); } diff --git a/core/src/net/sf/openrocket/startup/Preferences.java b/core/src/net/sf/openrocket/startup/Preferences.java index 738a423f6..b134e8380 100644 --- a/core/src/net/sf/openrocket/startup/Preferences.java +++ b/core/src/net/sf/openrocket/startup/Preferences.java @@ -124,6 +124,8 @@ public abstract class Preferences implements ChangeSource { public abstract void putString(String directory, String key, String value); + public abstract java.util.prefs.Preferences getNode(String nodeName); + /* * ****************************************************************************************** */ diff --git a/core/src/net/sf/openrocket/unit/UnitGroup.java b/core/src/net/sf/openrocket/unit/UnitGroup.java index 1acf0a6ae..ae1a895b9 100644 --- a/core/src/net/sf/openrocket/unit/UnitGroup.java +++ b/core/src/net/sf/openrocket/unit/UnitGroup.java @@ -278,6 +278,7 @@ public class UnitGroup { UNITS_ROLL = new UnitGroup(); UNITS_ROLL.addUnit(new GeneralUnit(1, "rad/s")); + UNITS_ROLL.addUnit(new GeneralUnit(Math.PI / 180, DEGREE + "/s")); UNITS_ROLL.addUnit(new GeneralUnit(2 * Math.PI, "r/s")); UNITS_ROLL.addUnit(new GeneralUnit(2 * Math.PI / 60, "rpm")); UNITS_ROLL.setDefaultUnit(1); 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/src/net/sf/openrocket/util/TestRockets.java b/core/src/net/sf/openrocket/util/TestRockets.java index fedb59f4a..f4fff0762 100644 --- a/core/src/net/sf/openrocket/util/TestRockets.java +++ b/core/src/net/sf/openrocket/util/TestRockets.java @@ -48,6 +48,7 @@ import net.sf.openrocket.rocketcomponent.TubeCoupler; import net.sf.openrocket.simulation.SimulationOptions; import net.sf.openrocket.simulation.customexpression.CustomExpression; import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.simulation.extension.impl.ScriptingExtension; import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; import net.sf.openrocket.simulation.listeners.SimulationListener; import net.sf.openrocket.startup.Application; @@ -241,7 +242,7 @@ public class TestRockets { } - public Rocket makeSmallFlyable() { + public static Rocket makeSmallFlyable() { double noseconeLength = 0.10, noseconeRadius = 0.01; double bodytubeLength = 0.20, bodytubeRadius = 0.01, bodytubeThickness = 0.001; @@ -281,8 +282,12 @@ public class TestRockets { String id = rocket.newFlightConfigurationID(); bodytube.setMotorMount(true); - Motor m = Application.getMotorSetDatabase().findMotors(null, null, "B4", Double.NaN, Double.NaN).get(0); - bodytube.getMotorConfiguration().get(id).setMotor(m); + MotorConfiguration motorConfig = new MotorConfiguration(); + ThrustCurveMotor motor = getTestMotor(); + motorConfig.setMotor(motor); + motorConfig.setEjectionDelay(5); + + bodytube.getMotorConfiguration().set(id, motorConfig); bodytube.setMotorOverhang(0.005); rocket.getDefaultConfiguration().setFlightConfigurationID(id); @@ -643,11 +648,7 @@ public class TestRockets { // create motor config and add a motor to it MotorConfiguration motorConfig = new MotorConfiguration(); - ThrustCurveMotor motor = new ThrustCurveMotor( - Manufacturer.getManufacturer("A"), - "F12X", "Desc", Motor.Type.UNKNOWN, new double[] {}, - 0.024, 0.07, new double[] { 0, 1, 2 }, new double[] { 0, 1, 0 }, - new Coordinate[] { Coordinate.NUL, Coordinate.NUL, Coordinate.NUL }, "digestA"); + ThrustCurveMotor motor = getTestMotor(); motorConfig.setMotor(motor); motorConfig.setEjectionDelay(5); @@ -685,11 +686,7 @@ public class TestRockets { // create motor config and add a motor to it MotorConfiguration motorConfig = new MotorConfiguration(); - ThrustCurveMotor motor = new ThrustCurveMotor( - Manufacturer.getManufacturer("A"), - "F12X", "Desc", Motor.Type.UNKNOWN, new double[] {}, - 0.024, 0.07, new double[] { 0, 1, 2 }, new double[] { 0, 1, 0 }, - new Coordinate[] { Coordinate.NUL, Coordinate.NUL, Coordinate.NUL }, "digestA"); + ThrustCurveMotor motor = getTestMotor(); motorConfig.setMotor(motor); motorConfig.setEjectionDelay(5); @@ -918,6 +915,20 @@ public class TestRockets { return OpenRocketDocumentFactory.createDocumentFromRocket(rocket); } + + public static OpenRocketDocument makeTestRocket_v107_withSimulationExtension(String script) { + Rocket rocket = makeSmallFlyable(); + OpenRocketDocument document = OpenRocketDocumentFactory.createDocumentFromRocket(rocket); + Simulation sim = new Simulation(rocket); + ScriptingExtension ext = new ScriptingExtension(); + ext.setEnabled(true); + ext.setLanguage("JavaScript"); + ext.setScript(script); + sim.getSimulationExtensions().add(ext); + document.addSimulation(sim); + return document; + } + /* * Create a new test rocket for testing OpenRocketSaver.estimateFileSize() */ @@ -991,4 +1002,15 @@ public class TestRockets { } + + + private static ThrustCurveMotor getTestMotor() { + return new ThrustCurveMotor( + Manufacturer.getManufacturer("A"), + "F12X", "Desc", Motor.Type.UNKNOWN, new double[] {}, + 0.024, 0.07, new double[] { 0, 1, 2 }, new double[] { 0, 1, 0 }, + new Coordinate[] { Coordinate.NUL, Coordinate.NUL, Coordinate.NUL }, "digestA"); + } + + } diff --git a/core/test/net/sf/openrocket/ServicesForTesting.java b/core/test/net/sf/openrocket/ServicesForTesting.java index 2d6ec6743..8f729ffce 100644 --- a/core/test/net/sf/openrocket/ServicesForTesting.java +++ b/core/test/net/sf/openrocket/ServicesForTesting.java @@ -4,6 +4,7 @@ import java.util.Collections; import java.util.Locale; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import java.util.prefs.BackingStoreException; import net.sf.openrocket.formatting.RocketDescriptor; import net.sf.openrocket.formatting.RocketDescriptorImpl; @@ -62,6 +63,8 @@ public class ServicesForTesting extends AbstractModule { public static class PreferencesForTesting extends Preferences { + private static java.util.prefs.Preferences root = null; + @Override public boolean getBoolean(String key, boolean defaultValue) { // TODO Auto-generated method stub @@ -151,5 +154,28 @@ public class ServicesForTesting extends AbstractModule { return null; } + @Override + public java.util.prefs.Preferences getNode(String nodeName) { + return getBaseNode().node(nodeName); + } + + private java.util.prefs.Preferences getBaseNode() { + if (root == null) { + final String name = "OpenRocket-unittest-" + System.currentTimeMillis(); + root = java.util.prefs.Preferences.userRoot().node(name); + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + try { + root.removeNode(); + } catch (BackingStoreException e) { + e.printStackTrace(); + } + } + }); + } + return root; + } + } } diff --git a/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java b/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java index 96f826c08..ca18850a1 100644 --- a/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java +++ b/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java @@ -28,6 +28,8 @@ import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.motor.Motor; import net.sf.openrocket.motor.ThrustCurveMotor; import net.sf.openrocket.plugin.PluginModule; +import net.sf.openrocket.simulation.extension.impl.ScriptingExtension; +import net.sf.openrocket.simulation.extension.impl.ScriptingUtil; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.TestRockets; @@ -47,6 +49,10 @@ public class OpenRocketSaverTest { private OpenRocketSaver saver = new OpenRocketSaver(); private static final String TMP_DIR = "./tmp/"; + public static final String SIMULATION_EXTENSION_SCRIPT = "// Test < &\n// >\n// supportedVersions = Arrays.asList(DocumentConfig.SUPPORTED_VERSIONS); List testedVersions = Arrays.asList(testedVersionsStr); diff --git a/core/test/net/sf/openrocket/simulation/extension/impl/TestScriptingUtil.java b/core/test/net/sf/openrocket/simulation/extension/impl/TestScriptingUtil.java new file mode 100644 index 000000000..bd20a5fa8 --- /dev/null +++ b/core/test/net/sf/openrocket/simulation/extension/impl/TestScriptingUtil.java @@ -0,0 +1,88 @@ +package net.sf.openrocket.simulation.extension.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import net.sf.openrocket.startup.MockPreferences; + +import org.junit.Before; +import org.junit.Test; + +public class TestScriptingUtil { + + private static final String HASH_JavaScript_foobar = "SHA-256:8f06133e0235d239355b5ca8ca0b43dece803c29b2a563222519d982abd3fc43"; + + private ScriptingUtil util; + + @Before + public void setup() { + util = new ScriptingUtil(); + util.prefs = new MockPreferences(); + } + + /* + * Note: This class assumes that the JRE supports JavaScript scripting. + */ + + @Test + public void testGetLanguage() { + assertEquals(null, util.getLanguage(null)); + assertEquals(null, util.getLanguage("")); + assertEquals(null, util.getLanguage("foobar")); + assertEquals("JavaScript", util.getLanguage("JavaScript")); + assertEquals("JavaScript", util.getLanguage("javascript")); + assertEquals("JavaScript", util.getLanguage("ECMAScript")); + assertEquals("JavaScript", util.getLanguage("js")); + } + + + @Test + public void testGetLanguages() { + assertTrue(util.getLanguages().size() >= 1); + assertTrue(util.getLanguages().contains("JavaScript")); + } + + @Test + public void testIsTrustedScript() { + util.setTrustedScript("JavaScript", "foobar", true); + assertTrue(util.isTrustedScript("JavaScript", "foobar")); + assertTrue(util.isTrustedScript("JavaScript", " \n foobar \n\t\r")); + assertFalse(util.isTrustedScript("JavaScript", "foo\nbar")); + assertFalse(util.isTrustedScript("Javascript", "foobar")); + + // Empty script is always considered trusted + assertFalse(util.isTrustedScript("foo", null)); + assertTrue(util.isTrustedScript("foo", "")); + assertTrue(util.isTrustedScript("foo", " \n\r\t ")); + } + + @Test + public void testSetTrustedScript() { + util.setTrustedScript("JavaScript", " \n foobar \n\r ", true); + assertTrue(util.prefs.getNode(ScriptingUtil.NODE_ID).getBoolean(HASH_JavaScript_foobar, false)); + util.setTrustedScript("JavaScript", " foobar ", false); + assertTrue(util.prefs.getNode(ScriptingUtil.NODE_ID).getBoolean(HASH_JavaScript_foobar, true)); + assertFalse(util.prefs.getNode(ScriptingUtil.NODE_ID).getBoolean(HASH_JavaScript_foobar, false)); + } + + @Test + public void testClearTrustedScripts() { + util.setTrustedScript("JavaScript", "foobar", true); + assertTrue(util.isTrustedScript("JavaScript", "foobar")); + util.clearTrustedScripts(); + assertFalse(util.isTrustedScript("JavaScript", "foobar")); + } + + @Test + public void testNormalize() { + assertEquals("foo", ScriptingUtil.normalize("foo")); + assertEquals("foo bar", ScriptingUtil.normalize(" \n\r\t foo \r bar \n\t\r ")); + } + + @Test + public void testHash() { + assertEquals("SHA-256:12e6a78889b96a16d305b8e4af81119545f89eccba5fb37cc3a1ec2c53eab514", ScriptingUtil.hash("JS", "")); + assertEquals("SHA-256:000753e5deb2d8fa80e602ca03bcdb8e12a6b14b2b4a4d0abecdc976ad26e3ef", ScriptingUtil.hash("foo", "1165")); + assertEquals(HASH_JavaScript_foobar, ScriptingUtil.hash("JavaScript", "foobar")); + } +} diff --git a/core/test/net/sf/openrocket/startup/MockPreferences.java b/core/test/net/sf/openrocket/startup/MockPreferences.java new file mode 100644 index 000000000..eabfe18d9 --- /dev/null +++ b/core/test/net/sf/openrocket/startup/MockPreferences.java @@ -0,0 +1,108 @@ +package net.sf.openrocket.startup; + +import java.util.Set; +import java.util.prefs.BackingStoreException; + +import net.sf.openrocket.material.Material; +import net.sf.openrocket.preset.ComponentPreset; +import net.sf.openrocket.preset.ComponentPreset.Type; +import net.sf.openrocket.util.BugException; + +public class MockPreferences extends Preferences { + + private final String NODENAME = "OpenRocket-test-mock"; + private final java.util.prefs.Preferences NODE; + + public MockPreferences() { + java.util.prefs.Preferences root = java.util.prefs.Preferences.userRoot(); + try { + if (root.nodeExists(NODENAME)) { + root.node(NODENAME).removeNode(); + } + } catch (BackingStoreException e) { + throw new BugException("Unable to clear preference node", e); + } + NODE = root.node(NODENAME); + } + + @Override + public boolean getBoolean(String key, boolean def) { + return NODE.getBoolean(key, def); + } + + @Override + public void putBoolean(String key, boolean value) { + NODE.putBoolean(key, value); + } + + @Override + public int getInt(String key, int def) { + return NODE.getInt(key, def); + } + + @Override + public void putInt(String key, int value) { + NODE.putInt(key, value); + } + + @Override + public double getDouble(String key, double def) { + return NODE.getDouble(key, def); + } + + @Override + public void putDouble(String key, double value) { + NODE.putDouble(key, value); + } + + @Override + public String getString(String key, String def) { + return NODE.get(key, def); + } + + @Override + public void putString(String key, String value) { + NODE.put(key, value); + } + + @Override + public String getString(String directory, String key, String def) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public void putString(String directory, String key, String value) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public java.util.prefs.Preferences getNode(String nodeName) { + return NODE.node(nodeName); + } + + @Override + public void addUserMaterial(Material m) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public Set getUserMaterials() { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public void removeUserMaterial(Material m) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public void setComponentFavorite(ComponentPreset preset, Type type, boolean favorite) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public Set getComponentFavorites(Type type) { + throw new UnsupportedOperationException("Not yet implemented"); + } + +} 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..8305c734c 100644 --- a/swing/.classpath +++ b/swing/.classpath @@ -1,25 +1,26 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/swing/build.xml b/swing/build.xml index 6b01804e6..d6c4f9d10 100644 --- a/swing/build.xml +++ b/swing/build.xml @@ -105,6 +105,7 @@ + diff --git a/swing/lib/rsyntaxtextarea-2.5.6.jar b/swing/lib/rsyntaxtextarea-2.5.6.jar new file mode 100644 index 000000000..b60def66e Binary files /dev/null and b/swing/lib/rsyntaxtextarea-2.5.6.jar differ diff --git a/swing/resources/datafiles/examples/Simulation extensions and scripting.ork b/swing/resources/datafiles/examples/Simulation extensions and scripting.ork new file mode 100644 index 000000000..a1592fc11 Binary files /dev/null and b/swing/resources/datafiles/examples/Simulation extensions and scripting.ork differ diff --git a/swing/resources/datafiles/examples/Simulation listeners.ork b/swing/resources/datafiles/examples/Simulation listeners.ork deleted file mode 100644 index d4fbae528..000000000 Binary files a/swing/resources/datafiles/examples/Simulation listeners.ork and /dev/null differ diff --git a/swing/src/net/sf/openrocket/gui/simulation/SimulationEditDialog.java b/swing/src/net/sf/openrocket/gui/simulation/SimulationEditDialog.java index 53468bb61..7f8307225 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()); + } } } } @@ -167,7 +170,7 @@ public class SimulationEditDialog extends JDialog { //// Launch conditions tabbedPane.addTab(trans.get("simedtdlg.tab.Launchcond"), new SimulationConditionsPanel(simulation[0])); //// Simulation options - tabbedPane.addTab(trans.get("simedtdlg.tab.Simopt"), new SimulationOptionsPanel(simulation[0])); + tabbedPane.addTab(trans.get("simedtdlg.tab.Simopt"), new SimulationOptionsPanel(document, simulation[0])); tabbedPane.setSelectedIndex(0); diff --git a/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java b/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java index b94748b0f..6a925edef 100644 --- a/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java +++ b/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java @@ -1,54 +1,70 @@ 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; +import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.document.Simulation; import net.sf.openrocket.gui.SpinnerEditor; 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.startup.Preferences; 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(); - + + private OpenRocketDocument document; final Simulation simulation; - - SimulationOptionsPanel(final Simulation simulation) { + + private JPanel currentExtensions; + + SimulationOptionsPanel(OpenRocketDocument document, final Simulation simulation) { super(new MigLayout("fill")); + this.document = document; this.simulation = simulation; - + final SimulationOptions conditions = simulation.getOptions(); - + JPanel sub, subsub; String tip; JLabel label; @@ -56,7 +72,7 @@ class SimulationOptionsPanel extends JPanel { JSpinner spin; UnitSelector unit; BasicSlider slider; - + // // Simulation options sub = new JPanel(new MigLayout("fill, gap rel unrel", "[grow][65lp!][30lp!][75lp!]", "")); @@ -64,38 +80,38 @@ class SimulationOptionsPanel extends JPanel { sub.setBorder(BorderFactory.createTitledBorder(trans .get("simedtdlg.border.Simopt"))); this.add(sub, "growx, growy, aligny 0"); - + // Separate panel for computation methods, as they use a different // layout subsub = new JPanel(new MigLayout("insets 0, fill", "[grow][min!][min!][]")); - + // // Calculation method: tip = trans.get("simedtdlg.lbl.ttip.Calcmethod"); label = new JLabel(trans.get("simedtdlg.lbl.Calcmethod")); label.setToolTipText(tip); subsub.add(label, "gapright para"); - + // // Extended Barrowman label = new JLabel(trans.get("simedtdlg.lbl.ExtBarrowman")); label.setToolTipText(tip); subsub.add(label, "growx, span 3, wrap"); - + // Simulation method tip = trans.get("simedtdlg.lbl.ttip.Simmethod1") + trans.get("simedtdlg.lbl.ttip.Simmethod2"); label = new JLabel(trans.get("simedtdlg.lbl.Simmethod")); label.setToolTipText(tip); subsub.add(label, "gapright para"); - + label = new JLabel("6-DOF Runge-Kutta 4"); label.setToolTipText(tip); subsub.add(label, "growx, span 3, wrap"); - + // // Geodetic calculation method: label = new JLabel(trans.get("simedtdlg.lbl.GeodeticMethod")); label.setToolTipText(trans.get("simedtdlg.lbl.ttip.GeodeticMethodTip")); subsub.add(label, "gapright para"); - + EnumModel gcsModel = new EnumModel( conditions, "GeodeticComputation"); final JComboBox gcsCombo = new JComboBox(gcsModel); @@ -110,7 +126,7 @@ class SimulationOptionsPanel extends JPanel { gcsCombo.addActionListener(gcsTTipListener); gcsTTipListener.actionPerformed(null); subsub.add(gcsCombo, "span 3, wrap para"); - + // // Time step: label = new JLabel(trans.get("simedtdlg.lbl.Timestep")); @@ -121,25 +137,25 @@ class SimulationOptionsPanel extends JPanel { .toStringUnit(RK4SimulationStepper.RECOMMENDED_TIME_STEP) + "."; label.setToolTipText(tip); - subsub.add(label,"gapright para"); - + subsub.add(label, "gapright para"); + m = new DoubleModel(conditions, "TimeStep", UnitGroup.UNITS_TIME_STEP, 0, 1); - + spin = new JSpinner(m.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); spin.setToolTipText(tip); subsub.add(spin, ""); - + unit = new UnitSelector(m); unit.setToolTipText(tip); subsub.add(unit, ""); slider = new BasicSlider(m.getSliderModel(0, 0.2)); slider.setToolTipText(tip); subsub.add(slider, "w 100"); - + sub.add(subsub, "spanx, wrap para"); - + // Reset to default button JButton button = new JButton(trans.get("simedtdlg.but.resettodefault")); // Reset the time step to its default value ( @@ -159,141 +175,227 @@ class SimulationOptionsPanel extends JPanel { GeodeticComputationStrategy.SPHERICAL)); } }); - + sub.add(button, "align left"); - - // 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"); - - // // 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(); + desc.setText(trans.get("simedtdlg.SimExt.desc")); + sub.add(desc, "aligny 0, hmin 100lp, growx, wrap para"); + + + final JButton addExtension = new JButton(trans.get("simedtdlg.SimExt.add")); + 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"); - - // // 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]); + sub.add(addExtension, "growx, wrap 0"); + + 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(); + SwingSimulationExtensionConfigurator configurator = findConfigurator(e); + if (configurator != null) { + configurator.configure(e, simulation, SwingUtilities.windowForComponent(SimulationOptionsPanel.this)); + } + } + }); + menu.add(item); } - listenerModel.fireContentsChanged(); } + } + + JMenu copyMenu = null; + for (Simulation sim : document.getSimulations()) { + if (!sim.getSimulationExtensions().isEmpty()) { + JMenu menu = new JMenu(sim.getName()); + for (final SimulationExtension ext : sim.getSimulationExtensions()) { + JMenuItem item = new JMenuItem(ext.getName()); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + SimulationExtension e = ext.clone(); + simulation.getSimulationExtensions().add(e); + updateCurrentExtensions(); + SwingSimulationExtensionConfigurator configurator = findConfigurator(e); + if (configurator != null) { + configurator.configure(e, simulation, SwingUtilities.windowForComponent(SimulationOptionsPanel.this)); + } + } + }); + menu.add(item); + } + + if (copyMenu == null) { + copyMenu = new JMenu(trans.get("simedtdlg.SimExt.copyExtension")); + } + copyMenu.add(menu); + } + } + if (copyMenu != null) { + basemenu.add(copyMenu); + } + + return basemenu; + } + + 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(); + + 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 (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"); + } + + 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>() { }); - sub.add(button, "sizegroup buttons, alignx 50%"); - - } - - private class ListenerCellRenderer extends JLabel implements - ListCellRenderer { - - @Override - public Component getListCellRendererComponent(JList list, Object value, - int index, boolean isSelected, boolean cellHasFocus) { - 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; + for (SwingSimulationExtensionConfigurator c : configurators) { + if (c.support(extension)) { + return c; } - - 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; } + 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()); - } - } - + } 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 ff67daefa..48f1ccf0e 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); @@ -78,6 +78,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/gui/util/SwingPreferences.java b/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java index 499620c9b..b4bd15cdf 100644 --- a/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java +++ b/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java @@ -45,8 +45,8 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences { for (String lang : new String[] { "en", "de", "es", "fr", "it", "ru", "cs", "pl", "ja", "pt", "tr" }) { list.add(new Locale(lang)); } - list.add(new Locale("zh","CN")); - list.add(new Locale("uk","UA")); + list.add(new Locale("zh", "CN")); + list.add(new Locale("uk", "UA")); SUPPORTED_LOCALES = Collections.unmodifiableList(list); } @@ -200,6 +200,7 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences { * @param nodeName the node name * @return the preferences object for that node */ + @Override public Preferences getNode(String nodeName) { return PREFNODE.node(nodeName); } @@ -418,11 +419,11 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences { public boolean computeFlightInBackground() { return PREFNODE.getBoolean("backgroundFlight", true); } - + public void setComputeFlightInBackground(boolean b) { PREFNODE.putBoolean("backgroundFlight", b); } - + public Simulation getBackgroundSimulation(Rocket rocket) { Simulation s = new Simulation(rocket); SimulationOptions cond = s.getOptions(); 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..8eb2ea1ae --- /dev/null +++ b/swing/src/net/sf/openrocket/simulation/extension/AbstractSwingSimulationExtensionConfigurator.java @@ -0,0 +1,88 @@ +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; + + private JDialog dialog; + + 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) { + 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); + close(); + GUIUtil.setNullModels(dialog); + dialog = null; + } + + /** + * Return a title for the dialog window. By default uses the extension's name. + */ + protected String getTitle(SimulationExtension extension, Simulation simulation) { + return extension.getName(); + } + + /** + * Return the dialog currently open. + */ + protected JDialog getDialog() { + return dialog; + } + + /** + * Called when the default dialog is closed. By default does nothing. + */ + protected void close() { + + } + + 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..8b7879526 --- /dev/null +++ b/swing/src/net/sf/openrocket/simulation/extension/impl/AirStartConfigurator.java @@ -0,0 +1,58 @@ +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"); + + + panel.add(new JLabel("Launch velocity:")); + + m = new DoubleModel(extension, "LaunchVelocity", UnitGroup.UNITS_VELOCITY, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "w 65lp!"); + + unit = new UnitSelector(m); + panel.add(unit, "w 25"); + + slider = new BasicSlider(m.getSliderModel(0, 150)); + 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; + } + +} diff --git a/swing/src/net/sf/openrocket/simulation/extension/impl/ScriptingConfigurator.java b/swing/src/net/sf/openrocket/simulation/extension/impl/ScriptingConfigurator.java new file mode 100644 index 000000000..1005e600e --- /dev/null +++ b/swing/src/net/sf/openrocket/simulation/extension/impl/ScriptingConfigurator.java @@ -0,0 +1,166 @@ +package net.sf.openrocket.simulation.extension.impl; + +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.util.Set; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JOptionPane; +import javax.swing.JPanel; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.gui.adaptors.BooleanModel; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.components.StyledLabel.Style; +import net.sf.openrocket.plugin.Plugin; +import net.sf.openrocket.simulation.extension.AbstractSwingSimulationExtensionConfigurator; + +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.fife.ui.rsyntaxtextarea.SyntaxConstants; +import org.fife.ui.rsyntaxtextarea.TokenMakerFactory; +import org.fife.ui.rtextarea.RTextScrollPane; + +import com.google.inject.Inject; + +@Plugin +public class ScriptingConfigurator extends AbstractSwingSimulationExtensionConfigurator { + + @Inject + private ScriptingUtil util; + + private JComboBox languageSelector; + private RSyntaxTextArea text; + private JCheckBox trusted; + + private ScriptingExtension extension; + private Simulation simulation; + + public ScriptingConfigurator() { + super(ScriptingExtension.class); + } + + @Override + protected JComponent getConfigurationComponent(final ScriptingExtension extension, Simulation simulation, JPanel panel) { + this.extension = extension; + this.simulation = simulation; + + panel.add(new StyledLabel(trans.get("SimulationExtension.scripting.language.label"), Style.BOLD), "spanx, split"); + + String[] languages = util.getLanguages().toArray(new String[0]); + languageSelector = new JComboBox(languages); + languageSelector.setEditable(false); + languageSelector.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + setLanguage((String) languageSelector.getSelectedItem()); + } + }); + panel.add(languageSelector, "wrap para"); + + + text = new RSyntaxTextArea(extension.getScript(), 20, 80); + text.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT); + text.setCodeFoldingEnabled(true); + text.setLineWrap(true); + text.setWrapStyleWord(true); + text.setEditable(true); + text.setCurrentLineHighlightColor(new Color(255, 255, 230)); + text.addFocusListener(new FocusListener() { + @Override + public void focusGained(FocusEvent event) { + } + + @Override + public void focusLost(FocusEvent event) { + String str = text.getText(); + if (!extension.getScript().equals(str)) { + extension.setScript(str); + } + } + }); + + RTextScrollPane scroll = new RTextScrollPane(text); + panel.add(scroll, "spanx, grow, wrap para"); + + + BooleanModel enabled = new BooleanModel(extension, "Enabled"); + JCheckBox check = new JCheckBox(enabled); + check.setText(trans.get("SimulationExtension.scripting.text.enabled")); + check.setToolTipText(trans.get("SimulationExtension.scripting.text.enabled.ttip")); + panel.add(check, "spanx, wrap rel"); + + trusted = new JCheckBox(trans.get("SimulationExtension.scripting.text.trusted")); + trusted.setSelected(util.isTrustedScript(extension.getLanguage(), extension.getScript())); + panel.add(trusted, "spanx, split"); + + panel.add(new JPanel(), "growx"); + + JButton button = new JButton(trans.get("SimulationExtension.scripting.text.trusted.clear")); + button.setToolTipText(trans.get("SimulationExtension.scripting.text.trusted.clear.ttip")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + util.clearTrustedScripts(); + JOptionPane.showMessageDialog(getDialog(), trans.get("SimulationExtension.scripting.text.trusted.cleared"), + trans.get("SimulationExtension.scripting.text.trusted.cleared.title"), JOptionPane.INFORMATION_MESSAGE); + } + }); + panel.add(button, "wrap rel"); + + + StyledLabel label = new StyledLabel(trans.get("SimulationExtension.scripting.text.trusted.msg"), -1, Style.ITALIC); + panel.add(label); + + setLanguage(util.getLanguage(extension.getLanguage())); + + return panel; + } + + @Override + protected void close() { + util.setTrustedScript(extension.getLanguage(), extension.getScript(), trusted.isSelected()); + } + + + private void setLanguage(String language) { + if (language == null) { + language = ""; + } + if (!language.equals(languageSelector.getSelectedItem())) { + languageSelector.setSelectedItem(language); + } + extension.setLanguage(language); + text.setSyntaxEditingStyle(findSyntaxLanguage(language)); + getDialog().setTitle(getTitle(extension, simulation)); + } + + private String findSyntaxLanguage(String language) { + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName(language); + + if (engine != null) { + Set supported = TokenMakerFactory.getDefaultInstance().keySet(); + for (String type : engine.getFactory().getMimeTypes()) { + if (supported.contains(type)) { + return type; + } + for (String match : supported) { + if (match.contains("/" + language.toLowerCase())) { + return match; + } + } + } + } + + return SyntaxConstants.SYNTAX_STYLE_NONE; + } + +} diff --git a/swing/src/net/sf/openrocket/utils/Scripting.java b/swing/src/net/sf/openrocket/utils/Scripting.java new file mode 100644 index 000000000..ca1a2993d --- /dev/null +++ b/swing/src/net/sf/openrocket/utils/Scripting.java @@ -0,0 +1,38 @@ +package net.sf.openrocket.utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.script.ScriptEngineFactory; +import javax.script.ScriptEngineManager; + +import org.fife.ui.rsyntaxtextarea.TokenMakerFactory; + +public class Scripting { + + public static void main(String[] args) { + System.out.println("Scripting APIs:"); + + ScriptEngineManager manager = new ScriptEngineManager(); + for (ScriptEngineFactory factory : manager.getEngineFactories()) { + System.out.println(" engineName=" + factory.getEngineName() + + " engineVersion=" + factory.getEngineVersion() + + " languageName=" + factory.getLanguageName() + + " languageVersion=" + factory.getLanguageVersion() + + " names=" + factory.getNames() + + " mimeTypes=" + factory.getMimeTypes() + + " extensions=" + factory.getExtensions()); + } + System.out.println(); + + System.out.println("RSyntaxTextArea supported syntax languages:"); + TokenMakerFactory f = TokenMakerFactory.getDefaultInstance(); + List list = new ArrayList(f.keySet()); + Collections.sort(list); + for (String type : list) { + System.out.println(" " + type); + } + System.out.println(); + } +}