Change package and import statements to match changes since switch to gradle Get rid of note pointing to documentation about listeners (it wasn't a valid hyperlink, and I can't find the documentation it thought it was pointing to in this documentation tree) A little bit of rewording here and there Get rid of src-extra/AirStart.java file
469 lines
22 KiB
ReStructuredText
469 lines
22 KiB
ReStructuredText
*********************
|
|
Simulation Extensions
|
|
*********************
|
|
|
|
By using OpenRocket's extension and listener mechanism, it's possible to modify the program itself to add features that
|
|
are not supported by the program as distributed; some extensions that have been created already provide the ability to
|
|
air-start a rocket, to add active roll control, and to calculate and save extra flight data.
|
|
|
|
This page will discuss extensions and simulations. We'll start by showing how a simulation is executed
|
|
(so you can get a taste of what's possible), and then document the process of creating the extension.
|
|
|
|
.. warning::
|
|
|
|
Writing an extension inserts new code into the program. It is entirely possible to disrupt a simulation in a way that
|
|
invalidates simulation results, or can even crash the program. Be careful!
|
|
|
|
.. contents:: Table of Contents
|
|
:depth: 2
|
|
:local:
|
|
:backlinks: none
|
|
|
|
----
|
|
|
|
Adding an Existing Extension to a Simulation
|
|
============================================
|
|
|
|
Extensions are added to a simulation through a menu in the "Simulation Options" tab.
|
|
|
|
1. Open a .ork file and go to the **Flight Simulations** tab.
|
|
2. Click the **Edit simulation** button to open the **Edit simulation** dialog.
|
|
3. Go to the **Simulation options** tab.
|
|
4. Click the **Add extension** button.
|
|
|
|
This will open a menu similar to the one in the following screenshot:
|
|
|
|
.. figure:: /img/user_guide/simulation_extensions/Extension-menu.png
|
|
:align: center
|
|
:width: 35%
|
|
:figclass: or-image-border
|
|
:alt: Extension menu
|
|
|
|
Extension menu.
|
|
|
|
Clicking on the name of an extension will add it to the simulation; if it has a configuration dialog the dialog will be opened:
|
|
|
|
.. figure:: /img/user_guide/simulation_extensions/Air-start-configuration.png
|
|
:align: center
|
|
:width: 45%
|
|
:figclass: or-image-border
|
|
:alt: Air-start configuration dialog
|
|
|
|
Air-start configuration dialog.
|
|
|
|
In the case of the air-start extension, the configuration dialog allows you to set the altitude and velocity at which
|
|
your simulation will begin. After you close the configuration dialog (if any), a new panel will be added to the
|
|
**Simulation options** pane, showing the new extension with buttons to reconfigure it, obtain information about it, or
|
|
remove it from the simulation:
|
|
|
|
.. figure:: /img/user_guide/simulation_extensions/Air-start-pane.png
|
|
:align: center
|
|
:width: 35%
|
|
:figclass: or-image-border
|
|
:alt: Air-start extension pane
|
|
|
|
Air-start extension pane.
|
|
|
|
----
|
|
|
|
Creating a New OpenRocket Extension
|
|
===================================
|
|
|
|
The remainder of this page will describe how a new simulation extension is created.
|
|
|
|
Preliminary Concepts
|
|
--------------------
|
|
|
|
Before we can discuss writing an extension, we need to briefly discuss some of the internals of OpenRocket. In particular,
|
|
we need to talk about the simulation status, flight data, and simulation listeners.
|
|
|
|
Simulation Status
|
|
~~~~~~~~~~~~~~~~~
|
|
|
|
As a simulation proceeds, it maintains its state in a
|
|
`SimulationStatus` object. This object contains
|
|
information about the rocket's current position, orientation,
|
|
velocity, simulation state, and the simulation's event queue. It also contains a
|
|
reference to a copy of the rocket design and its configuration. Any
|
|
simulation listener method (see below) may modify the state of
|
|
the rocket by changing the properties of the `SimulationStatus` object.
|
|
|
|
You can obtain current information regarding the state of the simulation by calling `get*()` methods. For instance, the
|
|
rocket's current position is returned by calling `getRocketPosition()`; the rocket's position can be changed by calling
|
|
`setRocketPosition(Coordinate position)`. All of the `get*()` and `set*()` methods can be found in
|
|
:file:`core/src/main/java/info/openrocket/core/simulation/SimulationStatus.java`. Note that while some information can be obtained in
|
|
this way, it is not as complete as that found in `FlightData` and `FlightDataBranch` objects.
|
|
|
|
Flight Data
|
|
~~~~~~~~~~~
|
|
|
|
OpenRocket refers to simulation variables as `FlightDataType`s, which are `List<Double>` objects with one list for each
|
|
simulation variable and one element in the list for each time step. To obtain a `FlightDataType`, for example the current
|
|
motor mass, from `flightData`, we call `flightData.get(FlightDataType.TYPE_MOTOR_MASS)`. The standard `FlightDataType`
|
|
lists are all created in `core/src/main/java/info/openrocket/core/simulation/FlightDataType.java`; the mechanism for creating a new
|
|
`FlightDataType` if needed for your extension will be described later.
|
|
|
|
Data from the current simulation step can be obtained with e.g. `flightData.getLast(FlightDataType.TYPE_MOTOR_MASS)`.
|
|
|
|
The simulation data for each stage of the rocket's flight is referred to as a `FlightDataBranch`. Every simulation has
|
|
at least one `FlightDataBranch` for its sustainer, and will have additional branches for its boosters.
|
|
|
|
Finally, the collection of all of the `FlightDataBranch` es and some summary data for the simulation is stored in a
|
|
`FlightData` object.
|
|
|
|
Flight Conditions
|
|
~~~~~~~~~~~~~~~~~
|
|
|
|
Current data regarding the aerodynamics of the flight itself are stored in a ``FlightConditions`` object. This includes
|
|
things like the velocity, angle of attack, and roll and pitch angle and rates. It also contains a reference to the
|
|
current ``AtmosphericConditions``.
|
|
|
|
Simulation Listeners
|
|
~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Simulation listeners are methods that OpenRocket calls at specified points in the computation to either record
|
|
information or modify the simulation state. These are divided into three interface classes, named ``SimulationListener``,
|
|
``SimulationComputationListener``, and ``SimulationEventListener``.
|
|
|
|
All of these interfaces are implemented by the abstract class ``AbstractSimulationListener``. This class provides empty
|
|
methods for all of the methods defined in the three interfaces, which are overridden as needed when writing a listener.
|
|
A typical listener method (which is actually in the Air-start listener), would be:
|
|
|
|
.. code-block:: java
|
|
|
|
public void startSimulation(SimulationStatus status) throws SimulationException {
|
|
status.setRocketPosition(new Coordinate(0, 0, getLaunchAltitude()));
|
|
status.setRocketVelocity(status.getRocketOrientationQuaternion().rotate(new Coordinate(0, 0, getLaunchVelocity())));
|
|
}
|
|
|
|
This method is called when the simulation is first started. It obtains the desired launch altitude and velocity from its
|
|
configuration, and inserts them into the simulation status to simulate an air-start.
|
|
|
|
The full set of listener methods, with documentation regarding when they are called, can be found in
|
|
:file:`core/src/main/java/info/openrocket/core/simulation/listeners/AbstractSimulationListener.java`.
|
|
|
|
The listener methods can have three return value types:
|
|
|
|
* The ``startSimulation()``, ``endSimulation()``, and ``postStep()`` are called at a specific point of the simulation. They are
|
|
void methods and do not return any value.
|
|
* The ``preStep()`` and event-related hook methods return a boolean value indicating whether the associated action should
|
|
be taken or not. A return value of ``true`` indicates that the action should be taken as normally would be (default),
|
|
``false`` will inhibit the action.
|
|
* The pre- and post-computation methods may return the computed value, either as an object or a double value. The
|
|
pre-computation methods allow pre-empting the entire computation, while the post-computation methods allow augmenting
|
|
the computed values. These methods may return ``null`` or ``Double.NaN`` to use the original values (default), or return
|
|
an overriding value.
|
|
|
|
Every listener receives a ``SimulationStatus`` (see above) object as the first argument, and may also have additional arguments.
|
|
|
|
Each listener method may also throw a ``SimulationException``. This is
|
|
considered an error during simulation (not a program bug),
|
|
and an error dialog is displayed to the user with the exception message. The simulation data produced thus far is not
|
|
stored in the simulation. Throwing a ``RuntimeException`` is considered a bug in the software and will result in a bug report dialog.
|
|
|
|
If a simulation listener wants to stop a simulation prematurely without an error condition, it needs to add a flight
|
|
event of type ``FlightEvent.SIMULATION_END`` to the simulation event queue:
|
|
|
|
.. code-block:: java
|
|
|
|
status.getEventQueue().add(new FlightEvent(FlightEvent.Type.SIMULATION_END, status.getSimulationTime(), null));
|
|
|
|
This will cause the simulation to be terminated normally.
|
|
|
|
Creating a New Simulation Extension
|
|
-----------------------------------
|
|
|
|
Creating an extension for OpenRocket requires writing three classes:
|
|
|
|
* A listener, which extends ``AbstractSimulationListener``. This will be the bulk of your extension, and performs all the real work.
|
|
* An extension, which extends ``AbstractSimulationExtension``. This inserts your listener into the simulation. Your listener
|
|
can (and ordinarily will) be private to your extension.
|
|
* A provider, which extends ``AbstractSimulationExtensionProvider``. This puts your extension into the menu described above.
|
|
|
|
In addition, if your extension will have a configuration GUI, you will need to write:
|
|
|
|
* A configurator, which extends ``AbstractSwingSimulationExtensionConfigurator<E>``
|
|
|
|
You can either create your extension outside the source tree and make sure it is in a directory that is in your Java
|
|
classpath when OpenRocket is executed, or you can insert it in the source tree and compile it along with OpenRocket.
|
|
Since all of OpenRocket's code is freely available, and reading the code for the existing extensions will be very helpful
|
|
in writing your own, the easiest approach is to simply insert it in the source tree. If you select this option, a very
|
|
logical place to put your extension is in :file:`core/src/main/java/info/openrocket/core/simulation/extension/`
|
|
|
|
The extension examples provided with OpenRocket are located in a
|
|
subdirectory of this named :file:`example/`.
|
|
|
|
Your configurator, if any, will logically go in :file:`swing/src/main/java/info/openrocket/swing/simulation/extension/`
|
|
|
|
Configurators for the example extensions are located in a subdirectory
|
|
of this named :file:`example/`.
|
|
|
|
Extension Example
|
|
-----------------
|
|
|
|
To make things concrete, we'll start by creating a simple example extension, to air-start a rocket from a hard-coded altitude.
|
|
Later, we'll add a configurator to the extension so we can set the launch altitude through a GUI at runtime. This is a
|
|
simplified version of the ``AirStart`` extension included in the extension
|
|
``example`` directory in the OpenRocket source code tree (that extension also sets a
|
|
start velocity).
|
|
|
|
.. code-block:: java
|
|
:linenos:
|
|
|
|
package info.openrocket.core.simulation.extension;
|
|
|
|
import info.openrocket.core.simulation.SimulationConditions;
|
|
import info.openrocket.core.simulation.SimulationStatus;
|
|
import info.openrocket.core.simulation.exception.SimulationException;
|
|
import info.openrocket.core.simulation.extension.AbstractSimulationExtension;
|
|
import info.openrocket.core.simulation.listeners.AbstractSimulationListener;
|
|
import info.openrocket.core.util.Coordinate;
|
|
|
|
/**
|
|
* Simulation extension that launches a rocket from a specific altitude.
|
|
*/
|
|
public class AirStartExample extends AbstractSimulationExtension {
|
|
|
|
public void initialize(SimulationConditions conditions) throws SimulationException {
|
|
conditions.getSimulationListenerList().add(new AirStartListener());
|
|
}
|
|
|
|
@Override
|
|
public String getName() {
|
|
return "Air-Start Example";
|
|
}
|
|
|
|
@Override
|
|
public String getDescription() {
|
|
return "Simple extension example for air-start";
|
|
}
|
|
|
|
private class AirStartListener extends AbstractSimulationListener {
|
|
|
|
@Override
|
|
public void startSimulation(SimulationStatus status) throws SimulationException {
|
|
status.setRocketPosition(new Coordinate(0, 0, 1000.0));
|
|
}
|
|
}
|
|
}
|
|
|
|
There are several important features in this example:
|
|
|
|
* The ``initialize()`` method in lines 15-17, which adds the listener to the simulation. This is the
|
|
only method that is actually required to be defined in your
|
|
extension, though any real extension (including this example) will
|
|
almost certainly have more.
|
|
* The ``getName()`` method in lines 19-22, which provides the extension's name. A default ``getName()`` is provided by
|
|
``AbstractSimulationExtension``, which simply uses the classname (so for this example, ``getName()`` would have returned
|
|
``"AirStartExample"`` if this method hadn't overridden it).
|
|
* The ``getDescription()`` method in lines 24-27, which provides a brief description of the purpose of the extension.
|
|
This is the method that provides the text for the :guilabel:`Info` button dialog shown in the first section of this page.
|
|
* The listener itself in lines 29-35, which provides a single ``startSimulation()`` method. When the simulation starts
|
|
executing, this listener is called, and the rocket is set to an altitude of 1000 meters.
|
|
|
|
This will create the extension when it's compiled, but it won't put it in the simulation extension menu. To be able to
|
|
actually use it, we need a provider, like this:
|
|
|
|
.. code-block:: java
|
|
:linenos:
|
|
|
|
import info.openrocket.core.plugin.Plugin;
|
|
import info.openrocket.core.simulation.extension.AbstractSimulationExtensionProvider;
|
|
|
|
@Plugin
|
|
public class AirStartExampleProvider extends AbstractSimulationExtensionProvider {
|
|
public AirStartExampleProvider() {
|
|
super(AirStartExample.class, "Launch conditions", "Air-start example");
|
|
}
|
|
}
|
|
|
|
This class adds your extension to the extension menu with the
|
|
``super`` call in line 7. The first parameter (``"Launch Conditions"``) is the first menu level,
|
|
while the second (``"Air-start example"``) is the actual menu entry. These strings can be anything you want; using a
|
|
first level entry that didn't previously exist will add it to the first level menu.
|
|
|
|
Try it! Putting the extension in a file named :file:`core/src/main/java/info/openrocket/core/simulation/extension/AirStartExample.java`
|
|
and the provider in
|
|
:file:`core/src/main/java/info/openrocket/core/simulation/extension/AirStartExampleProvider.java`,
|
|
and compiling, and running OpenRocket will give you a new entry in the extensions menu; adding it to the simulation will cause your simulation to
|
|
start at an altitude of 1000 meters.
|
|
|
|
Adding a Configurator
|
|
---------------------
|
|
|
|
To be able to configure the extension at runtime, we need to write a configurator and provide it with a way to
|
|
communicate with the extension. First, we'll modify the extension as follows:
|
|
|
|
.. code-block:: java
|
|
:linenos:
|
|
|
|
package info.openrocket.core.simulation.extension;
|
|
|
|
import info.openrocket.core.simulation.SimulationConditions;
|
|
import info.openrocket.core.simulation.SimulationStatus;
|
|
import info.openrocket.core.simulation.exception.SimulationException;
|
|
import info.openrocket.core.simulation.extension.AbstractSimulationExtension;
|
|
import info.openrocket.core.simulation.listeners.AbstractSimulationListener;
|
|
import info.openrocket.core.util.Coordinate;
|
|
|
|
/**
|
|
* Simulation extension that launches a rocket from a specific altitude.
|
|
*/
|
|
public class AirStartExample extends AbstractSimulationExtension {
|
|
|
|
public void initialize(SimulationConditions conditions) throws SimulationException {
|
|
conditions.getSimulationListenerList().add(new AirStartListener());
|
|
}
|
|
|
|
@Override
|
|
public String getName() {
|
|
return "Air-Start Example";
|
|
}
|
|
|
|
@Override
|
|
public String getDescription() {
|
|
return "Simple extension example for air-start";
|
|
}
|
|
|
|
public double getLaunchAltitude() {
|
|
return config.getDouble("launchAltitude", 1000.0);
|
|
}
|
|
|
|
public void setLaunchAltitude(double launchAltitude) {
|
|
config.put("launchAltitude", launchAltitude);
|
|
fireChangeEvent();
|
|
}
|
|
|
|
private class AirStartListener extends AbstractSimulationListener {
|
|
|
|
@Override
|
|
public void startSimulation(SimulationStatus status) throws SimulationException {
|
|
status.setRocketPosition(new Coordinate(0, 0, getLaunchAltitude()));
|
|
}
|
|
}
|
|
}
|
|
|
|
This adds two methods to the extension (``getLaunchAltitude()`` and ``setLaunchAltitude()``), and calls ``getLaunchAltitude()``
|
|
from within the listener to obtain the configured launch altitude. ``config`` is a ``Config`` object, provided by
|
|
``AbstractSimulationExtension`` (so it isn't necessary to call a constructor yourself).
|
|
:file:`core/src/main/java/info/openrocket/core/util/Config.java` includes methods to interact with a configurator, allowing the
|
|
extension to obtain ``double``, ``string``, and other configuration values.
|
|
|
|
In this case, we'll only be defining a single configuration field in our configurator, ``"launchAltitude"``.
|
|
|
|
The ``getLaunchAltitude()`` method obtains the air-start altitude for the simulation from the configuration, and sets a
|
|
default air-start altitude of 1000 meters. Our ``startSimulation()`` method has been modified to make use of this to obtain
|
|
the user's requested air-start altitude from the configurator, in place of the original hard-coded value.
|
|
|
|
The ``setLaunchAltitude()`` method places a new launch altitude in the configuration. While our extension doesn't make
|
|
use of this method directly, it will be necessary for our configurator. The call to ``fireChangeEvent()`` in this method
|
|
assures that the changes we make to the air-start altitude are propagated throughout the simulation.
|
|
|
|
The configurator itself looks like this:
|
|
|
|
.. code-block:: java
|
|
:linenos:
|
|
|
|
package info.openrocket.swing.simulation.extension;
|
|
|
|
import javax.swing.JComponent;
|
|
import javax.swing.JLabel;
|
|
import javax.swing.JPanel;
|
|
import javax.swing.JSpinner;
|
|
|
|
import info.openrocket.core.document.Simulation;
|
|
import info.openrocket.core.simulation.extension.AirStartExample;
|
|
import info.openrocket.swing.gui.SpinnerEditor;
|
|
import info.openrocket.swing.gui.adaptors.DoubleModel;
|
|
import info.openrocket.swing.gui.components.BasicSlider;
|
|
import info.openrocket.swing.gui.components.UnitSelector;
|
|
import info.openrocket.core.plugin.Plugin;
|
|
import info.openrocket.swing.simulation.extension.AbstractSwingSimulationExtensionConfigurator;
|
|
import info.openrocket.core.unit.UnitGroup;
|
|
|
|
@Plugin
|
|
public class AirStartExampleConfigurator extends AbstractSwingSimulationExtensionConfigurator<AirStartExample> {
|
|
|
|
public AirStartExampleConfigurator() {
|
|
super(AirStartExample.class);
|
|
}
|
|
|
|
@Override
|
|
protected JComponent getConfigurationComponent(AirStartExample 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, 5000));
|
|
panel.add(slider, "w 75lp, wrap");
|
|
|
|
return panel;
|
|
}
|
|
}
|
|
|
|
After some boilerplate, this class creates a new ``DoubleModel`` to
|
|
manage the air-start altitude (line 29). The most important things
|
|
to notice about the ``DoubleModel`` constructor are the parameters ``"LaunchAltitude"`` and ``UnitGroup.UNITS_DISTANCE``.
|
|
|
|
* ``"LaunchAltitude"`` is used by the system to synthesize calls to the ``getLaunchAltitude()`` and ``setLaunchAltitude()``
|
|
methods defined in ``AirStartExample`` above. The name of the ``DoubleModel``, ``"LaunchAltitude"``, **MUST** match the names of the corresponding
|
|
``set`` and ``get`` methods exactly. If they don't, there will be an exception at runtime when the user attempts to change the value.
|
|
* ``UnitGroup.UNITS_DISTANCE`` specifies the unit group to be used by this ``DoubleModel``. OpenRocket uses SI (MKS) units internally,
|
|
but allows users to select the units they wish to use for their interface. Specifying a ``UnitGroup`` provides the conversions
|
|
and unit displays for the interface. The available ``UnitGroup`` options are defined in :file:`core/src/main/java/info/openrocket/core/unit/UnitGroup.java`
|
|
|
|
The remaining code in this method creates a ``JSpinner``, a ``UnitSelector``, and a ``BasicSlider`` all referring to this ``DoubleModel``.
|
|
When the resulting configurator is displayed, it looks like this:
|
|
|
|
.. figure:: /img/user_guide/simulation_extensions/Example_Configurator.png
|
|
:align: center
|
|
:width: 45%
|
|
:figclass: or-image-border
|
|
:alt: Example configurator
|
|
|
|
Example configurator.
|
|
|
|
The surrounding Dialog window and the **Close** button are provided by the system.
|
|
|
|
----
|
|
|
|
Example User Extensions Provided With OpenRocket
|
|
================================================
|
|
|
|
Several examples of user extensions are provided in the OpenRocket source tree. As mentioned previously, the extensions
|
|
are all located in :file:`core/src/main/java/info/openrocket/core/simulation/extension/example/` and their configurators are all located
|
|
in :file:`swing/src/main/java/info/openrocket/swing/simulation/extension/example/`. Also recall that every extension has a corresponding
|
|
provider.
|
|
|
|
.. list-table::
|
|
:header-rows: 1
|
|
|
|
* - Purpose
|
|
- Extension
|
|
- Configurator
|
|
* - Set air-start altitude and velocity
|
|
- `AirStart.java`
|
|
- `AirStartConfigurator.java`
|
|
* - Save some simulation values as a CSV file
|
|
- `CSVSave.java`
|
|
- *(none)*
|
|
* - Calculate damping moment coefficient after every simulation step
|
|
- `DampingMoment.java`
|
|
- *(none)*
|
|
* - Print summary of simulation progress after each step
|
|
- `PrintSimulation.java`
|
|
- *(none)*
|
|
* - Active roll control
|
|
- `RollControl.java`
|
|
- `RollControlConfigurator.java`
|
|
* - Stop simulation at specified time or number of steps
|
|
- `StopSimulation`
|
|
- `StopSimulationConfigurator`
|