From a2c0af3b7fc645c2299b76190b7ef8d612d32056 Mon Sep 17 00:00:00 2001 From: Sampo Niskanen Date: Sat, 27 Dec 2014 19:18:53 +0200 Subject: [PATCH] Initial implementation of scripting extension --- core/resources/l10n/messages.properties | 4 + .../extension/impl/ScriptingExtension.java | 72 ++++++ .../extension/impl/ScriptingProvider.java | 13 ++ .../impl/ScriptingSimulationListener.java | 217 ++++++++++++++++++ .../sf/openrocket/startup/Application.java | 5 + .../extension/impl/ScriptingConfigurator.java | 54 +++++ 6 files changed, 365 insertions(+) create mode 100644 core/src/net/sf/openrocket/simulation/extension/impl/ScriptingExtension.java create mode 100644 core/src/net/sf/openrocket/simulation/extension/impl/ScriptingProvider.java create mode 100644 core/src/net/sf/openrocket/simulation/extension/impl/ScriptingSimulationListener.java create mode 100644 swing/src/net/sf/openrocket/simulation/extension/impl/ScriptingConfigurator.java diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index 0ace129c9..eba889dc0 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -405,6 +405,10 @@ 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.script.label = JavaScript code: + SimulationEditDialog.btn.plot = Plot SimulationEditDialog.btn.export = Export SimulationEditDialog.btn.edit = Edit 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..4cd1c7ef7 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingExtension.java @@ -0,0 +1,72 @@ +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.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; + +public class ScriptingExtension extends AbstractSimulationExtension { + + private static final String JS = "JavaScript"; + + public ScriptingExtension() { + setLanguage(JS); + } + + @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 initialize(SimulationConditions conditions) throws SimulationException { + conditions.getSimulationListenerList().add(getListener()); + } + + + public String getScript() { + return config.getString("script", ""); + } + + public void setScript(String script) { + config.put("script", script); + } + + public String getLanguage() { + // TODO: Support other languages + return JS; + } + + public void setLanguage(String language) { + config.put("language", language); + } + + + SimulationListener getListener() throws SimulationException { + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName("JavaScript"); + + try { + engine.eval(getScript()); + } catch (ScriptException e) { + throw new SimulationException("Invalid script: " + e.getMessage()); + } + + // TODO: Check for implementation first + 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..9ca6edcd6 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingSimulationListener.java @@ -0,0 +1,217 @@ +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.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(null, "startSimulation", status); + } + + @Override + public void endSimulation(SimulationStatus status, SimulationException exception) { + try { + invoke(null, "endSimulation", status, exception); + } catch (SimulationException e) { + } + } + + @Override + public boolean preStep(SimulationStatus status) throws SimulationException { + return invoke(true, "preStep", status); + } + + @Override + public void postStep(SimulationStatus status) throws SimulationException { + invoke(null, "postStep", status); + } + + + + //// SimulationEventListener //// + + @Override + public boolean addFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { + return invoke(true, "addFlightEvent", status, event); + } + + @Override + public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { + return invoke(true, "handleFlightEvent", status, event); + } + + @Override + public boolean motorIgnition(SimulationStatus status, MotorId motorId, MotorMount mount, MotorInstance instance) throws SimulationException { + return invoke(true, "motorIgnition", status, motorId, mount, instance); + } + + @Override + public boolean recoveryDeviceDeployment(SimulationStatus status, RecoveryDevice recoveryDevice) throws SimulationException { + return invoke(true, "recoveryDeviceDeployment", status, recoveryDevice); + } + + + + //// SimulationComputationListener //// + + @Override + public AccelerationData preAccelerationCalculation(SimulationStatus status) throws SimulationException { + return invoke(null, "preAccelerationCalculation", status); + } + + @Override + public AerodynamicForces preAerodynamicCalculation(SimulationStatus status) throws SimulationException { + return invoke(null, "preAerodynamicCalculation", status); + } + + @Override + public AtmosphericConditions preAtmosphericModel(SimulationStatus status) throws SimulationException { + return invoke(null, "preAtmosphericModel", status); + } + + @Override + public FlightConditions preFlightConditions(SimulationStatus status) throws SimulationException { + return invoke(null, "preFlightConditions", status); + } + + @Override + public double preGravityModel(SimulationStatus status) throws SimulationException { + return invoke(Double.NaN, "preGravityModel", status); + } + + @Override + public MassData preMassCalculation(SimulationStatus status) throws SimulationException { + return invoke(null, "preMassCalculation", status); + } + + @Override + public double preSimpleThrustCalculation(SimulationStatus status) throws SimulationException { + return invoke(Double.NaN, "preSimpleThrustCalculation", status); + } + + @Override + public Coordinate preWindModel(SimulationStatus status) throws SimulationException { + return invoke(null, "preWindModel", status); + } + + @Override + public AccelerationData postAccelerationCalculation(SimulationStatus status, AccelerationData acceleration) throws SimulationException { + return invoke(null, "postAccelerationCalculation", status, acceleration); + } + + @Override + public AerodynamicForces postAerodynamicCalculation(SimulationStatus status, AerodynamicForces forces) throws SimulationException { + return invoke(null, "postAerodynamicCalculation", status, forces); + } + + @Override + public AtmosphericConditions postAtmosphericModel(SimulationStatus status, AtmosphericConditions atmosphericConditions) throws SimulationException { + return invoke(null, "postAtmosphericModel", status, atmosphericConditions); + } + + @Override + public FlightConditions postFlightConditions(SimulationStatus status, FlightConditions flightConditions) throws SimulationException { + return invoke(null, "postFlightConditions", status, flightConditions); + } + + @Override + public double postGravityModel(SimulationStatus status, double gravity) throws SimulationException { + return invoke(Double.NaN, "postGravityModel", status, gravity); + } + + @Override + public MassData postMassCalculation(SimulationStatus status, MassData massData) throws SimulationException { + return invoke(null, "postMassCalculation", status, massData); + } + + @Override + public double postSimpleThrustCalculation(SimulationStatus status, double thrust) throws SimulationException { + return invoke(Double.NaN, "postSimpleThrustCalculation", status, thrust); + } + + @Override + public Coordinate postWindModel(SimulationStatus status, Coordinate wind) throws SimulationException { + return invoke(null, "postWindModel", status, wind); + } + + + @SuppressWarnings("unchecked") + private T invoke(T def, String method, Object... args) throws SimulationException { + try { + if (!missing.contains(method)) { + return (T) invocable.invokeFunction(method, args); + } + } 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/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/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..b567b37f5 --- /dev/null +++ b/swing/src/net/sf/openrocket/simulation/extension/impl/ScriptingConfigurator.java @@ -0,0 +1,54 @@ +package net.sf.openrocket.simulation.extension.impl; + +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; + +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.components.StyledLabel.Style; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.plugin.Plugin; +import net.sf.openrocket.simulation.extension.AbstractSwingSimulationExtensionConfigurator; + +@Plugin +public class ScriptingConfigurator extends AbstractSwingSimulationExtensionConfigurator { + + protected ScriptingConfigurator() { + super(ScriptingExtension.class); + } + + @Override + protected JComponent getConfigurationComponent(final ScriptingExtension extension, Simulation simulation, JPanel panel) { + + panel.add(new StyledLabel(trans.get("SimulationExtension.scripting.script.label"), Style.BOLD), "wrap"); + + final JTextArea text = new JTextArea(extension.getScript(), 20, 60); + text.setLineWrap(true); + text.setWrapStyleWord(true); + text.setEditable(true); + GUIUtil.setTabToFocusing(text); + 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); + } + } + }); + + panel.add(new JScrollPane(text), "grow"); + + return panel; + } + +}