diff --git a/core/src/main/java/info/openrocket/core/document/Simulation.java b/core/src/main/java/info/openrocket/core/document/Simulation.java
index c661ace65..a05125565 100644
--- a/core/src/main/java/info/openrocket/core/document/Simulation.java
+++ b/core/src/main/java/info/openrocket/core/document/Simulation.java
@@ -357,7 +357,7 @@ public class Simulation implements ChangeSource, Cloneable {
public boolean hasErrors() {
FlightData data = getSimulatedData();
for (int branchNo = 0; branchNo < data.getBranchCount(); branchNo++) {
- if (data.getBranch(branchNo).getFirstEvent(FlightEvent.Type.SIM_ABORT) != null) {
+ if (hasErrors(branchNo)) {
return true;
}
}
diff --git a/core/src/main/java/info/openrocket/core/file/CSVExport.java b/core/src/main/java/info/openrocket/core/file/CSVExport.java
index 6fd26e23c..39bade149 100644
--- a/core/src/main/java/info/openrocket/core/file/CSVExport.java
+++ b/core/src/main/java/info/openrocket/core/file/CSVExport.java
@@ -259,8 +259,12 @@ public class CSVExport {
private static void printEvent(PrintWriter writer, FlightEvent e,
String commentStarter) {
- writer.println(prependComment(commentStarter, "Event " + e.getType().name() +
+ writer.print(prependComment(commentStarter, "Event " + e.getType().name() +
" occurred at t=" + TextUtil.doubleToString(e.getTime()) + " seconds"));
+ if (e.getType() == FlightEvent.Type.SIM_WARN) {
+ writer.print(": " + (Warning) e.getData());
+ }
+ writer.println();
}
private static void writeSimulationComments(PrintWriter writer,
diff --git a/core/src/main/java/info/openrocket/core/file/openrocket/OpenRocketSaver.java b/core/src/main/java/info/openrocket/core/file/openrocket/OpenRocketSaver.java
index d8f8d2285..1891a92a0 100644
--- a/core/src/main/java/info/openrocket/core/file/openrocket/OpenRocketSaver.java
+++ b/core/src/main/java/info/openrocket/core/file/openrocket/OpenRocketSaver.java
@@ -430,7 +430,24 @@ public class OpenRocketSaver extends RocketSaver {
indent++;
for (Warning w : data.getWarningSet()) {
- writeElementWithAttribute("warning", "priority", w.getPriority().getExportLabel(), TextUtil.escapeXML(w.toString()));
+ writeln("");
+ indent++;
+
+ writeElement("id", w.getID().toString());
+ writeElement("description", w.getMessageDescription());
+ writeElement("priority", w.getPriority());
+
+ if (null != w.getSources()) {
+ for (RocketComponent c : w.getSources()) {
+ writeElement("source", c.getID());
+ }
+ }
+
+ // We write the whole string content for backwards compatibility with old versions
+ writeln(TextUtil.escapeXML(w.toString()));
+
+ indent--;
+ writeln("");
}
// Check whether to store data
@@ -606,16 +623,21 @@ public class OpenRocketSaver extends RocketSaver {
// Write events
for (FlightEvent event : branch.getEvents()) {
String eventStr = "";
+ eventStr += "/>";
writeln(eventStr);
}
@@ -676,14 +698,6 @@ public class OpenRocketSaver extends RocketSaver {
content = "";
writeln("<" + element + ">" + TextUtil.escapeXML(content) + "" + element + ">");
}
-
- private void writeElementWithAttribute(String element, String attributeName, String attribute, Object content) throws IOException {
- content = content == null ? "" : content;
-
- writeln("<" + element + " " + attributeName + " = \"" + attribute + "\">" + TextUtil.escapeXML(content) + "" + element + ">");
- }
-
-
private void writeln(String str) throws IOException {
if (str.length() == 0) {
diff --git a/core/src/main/java/info/openrocket/core/file/openrocket/importt/FlightDataBranchHandler.java b/core/src/main/java/info/openrocket/core/file/openrocket/importt/FlightDataBranchHandler.java
index 7ba4088b8..2b2277ce1 100644
--- a/core/src/main/java/info/openrocket/core/file/openrocket/importt/FlightDataBranchHandler.java
+++ b/core/src/main/java/info/openrocket/core/file/openrocket/importt/FlightDataBranchHandler.java
@@ -3,6 +3,7 @@ package info.openrocket.core.file.openrocket.importt;
import java.util.HashMap;
import java.util.UUID;
+import info.openrocket.core.logging.Message;
import info.openrocket.core.logging.SimulationAbort;
import info.openrocket.core.logging.SimulationAbort.Cause;
import info.openrocket.core.logging.WarningSet;
@@ -132,15 +133,14 @@ class FlightDataBranchHandler extends AbstractElementHandler {
if (element.equals("event")) {
double time;
FlightEvent.Type type;
- SimulationAbort abort = null;
- SimulationAbort.Cause cause = null;
+ Message data = null;
RocketComponent source = null;
String sourceID;
try {
time = DocumentConfig.stringToDouble(attributes.get("time"));
} catch (NumberFormatException e) {
- warnings.add("Illegal event specification, ignoring.");
+ warnings.add("Illegal event time specification, ignoring: " + e.getMessage());
return;
}
@@ -157,13 +157,23 @@ class FlightDataBranchHandler extends AbstractElementHandler {
source = rocket.findComponent(UUID.fromString(sourceID));
}
+ // For warning events, get the warning
+ if (type == FlightEvent.Type.SIM_WARN) {
+ data = simHandler.getWarningSet().findById(UUID.fromString(attributes.get("id")));
+ }
+
// For aborts, get the cause
- cause = (Cause) DocumentConfig.findEnum(attributes.get("cause"), SimulationAbort.Cause.class);
+ Cause cause = (Cause) DocumentConfig.findEnum(attributes.get("cause"), SimulationAbort.Cause.class);
if (cause != null) {
- abort = new SimulationAbort(cause);
+ data = new SimulationAbort(cause);
}
- branch.addEvent(new FlightEvent(type, time, source, abort));
+ try {
+ branch.addEvent(new FlightEvent(type, time, source, data));
+ } catch (Exception e) {
+ warnings.add("Illegal parameters for FlightEvent: " + e.getMessage());
+ }
+
return;
}
diff --git a/core/src/main/java/info/openrocket/core/file/openrocket/importt/FlightDataHandler.java b/core/src/main/java/info/openrocket/core/file/openrocket/importt/FlightDataHandler.java
index 6ce1573f1..1c6ad1b89 100644
--- a/core/src/main/java/info/openrocket/core/file/openrocket/importt/FlightDataHandler.java
+++ b/core/src/main/java/info/openrocket/core/file/openrocket/importt/FlightDataHandler.java
@@ -39,7 +39,7 @@ class FlightDataHandler extends AbstractElementHandler {
WarningSet warnings) {
if (element.equals("warning")) {
- return PlainTextHandler.INSTANCE;
+ return new WarningHandler(context.getOpenRocketDocument().getRocket(), warningSet);
}
if (element.equals("databranch")) {
if (attributes.get("name") == null || attributes.get("types") == null) {
@@ -83,10 +83,10 @@ class FlightDataHandler extends AbstractElementHandler {
if (branch.getLength() > 0) {
branches.add(branch);
}
- } else if (element.equals("warning")) {
- String priorityStr = attributes.get("priority");
- MessagePriority priority = MessagePriority.fromExportLabel(priorityStr);
- warningSet.add(Warning.fromString(content, priority));
+ // } else if (element.equals("warning")) {
+ // String priorityStr = attributes.get("priority");
+ // MessagePriority priority = MessagePriority.fromExportLabel(priorityStr);
+ // warningSet.add(Warning.fromString(content, priority));
}
}
@@ -157,6 +157,9 @@ class FlightDataHandler extends AbstractElementHandler {
data.getWarningSet().addAll(warningSet);
data.immute();
}
-
-
+
+
+ public WarningSet getWarningSet() {
+ return warningSet;
+ }
}
diff --git a/core/src/main/java/info/openrocket/core/file/openrocket/importt/SingleSimulationHandler.java b/core/src/main/java/info/openrocket/core/file/openrocket/importt/SingleSimulationHandler.java
index f283144f1..80e062b03 100644
--- a/core/src/main/java/info/openrocket/core/file/openrocket/importt/SingleSimulationHandler.java
+++ b/core/src/main/java/info/openrocket/core/file/openrocket/importt/SingleSimulationHandler.java
@@ -156,6 +156,13 @@ class SingleSimulationHandler extends AbstractElementHandler {
doc.addSimulation(simulation);
}
+ /**
+ * @return the warning set associated with this simulation
+ */
+ public WarningSet getWarningSet() {
+ return dataHandler.getWarningSet();
+ }
+
private SimulationExtension compatibilityExtension(String className) {
JavaCode extension = Application.getInjector().getInstance(JavaCode.class);
extension.setClassName(className);
diff --git a/core/src/main/java/info/openrocket/core/file/openrocket/importt/WarningHandler.java b/core/src/main/java/info/openrocket/core/file/openrocket/importt/WarningHandler.java
new file mode 100644
index 000000000..edfde5bc5
--- /dev/null
+++ b/core/src/main/java/info/openrocket/core/file/openrocket/importt/WarningHandler.java
@@ -0,0 +1,73 @@
+package info.openrocket.core.file.openrocket.importt;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.UUID;
+
+import info.openrocket.core.file.simplesax.AbstractElementHandler;
+import info.openrocket.core.file.simplesax.ElementHandler;
+import info.openrocket.core.file.simplesax.PlainTextHandler;
+import info.openrocket.core.logging.MessagePriority;
+import info.openrocket.core.logging.Warning;
+import info.openrocket.core.logging.WarningSet;
+import info.openrocket.core.rocketcomponent.Rocket;
+import info.openrocket.core.rocketcomponent.RocketComponent;
+
+class WarningHandler extends AbstractElementHandler {
+ private Rocket rocket;
+ private WarningSet warningSet;
+ private Warning warning;
+ private UUID id = UUID.randomUUID();
+ private MessagePriority priority = MessagePriority.NORMAL;
+ private ArrayList sources = new ArrayList<>();
+ private String warningText;
+
+ public WarningHandler(Rocket rocket, WarningSet warningSet) {
+ this.rocket = rocket;
+ this.warningSet = warningSet;
+ }
+
+ @Override
+ public ElementHandler openElement(String element, HashMap attributes,
+ WarningSet warnings) {
+ return PlainTextHandler.INSTANCE;
+ }
+
+ @Override
+ public void closeElement(String element, HashMap attributes,
+ String content, WarningSet warnings) {
+ if (element.equals("id")) {
+ id = UUID.fromString(content);
+ } else if (element.equals("description")) {
+ warning = Warning.fromString(content);
+ } else if (element.equals("priority")) {
+ priority = MessagePriority.fromExportLabel(content);
+ } else if (element.equals("source")) {
+ RocketComponent component = rocket.findComponent(UUID.fromString(content));
+ sources.add(component);
+ } else {
+ warnings.add("Unknown element '" + element + "', ignoring.");
+ }
+ }
+
+ @Override
+ public void endHandler(String element, HashMap attributes,
+ String content, WarningSet warnings) {
+
+ // If we didn't already create a warning, this came from an old version
+ if (null == warning) {
+ warning = Warning.fromString(content.trim());
+ }
+ if (null != id) {
+ warning.setID(id);
+ }
+ if (null != priority) {
+ warning.setPriority(priority);
+ }
+ if (null != sources) {
+ warning.setSources(sources.toArray(new RocketComponent[0]));
+ }
+
+ warningSet.add(warning);
+ }
+}
diff --git a/core/src/main/java/info/openrocket/core/file/simplesax/ElementHandler.java b/core/src/main/java/info/openrocket/core/file/simplesax/ElementHandler.java
index 3f4e13326..38bb894ec 100644
--- a/core/src/main/java/info/openrocket/core/file/simplesax/ElementHandler.java
+++ b/core/src/main/java/info/openrocket/core/file/simplesax/ElementHandler.java
@@ -80,4 +80,4 @@ public interface ElementHandler {
public abstract void endHandler(String element, HashMap attributes,
String content, WarningSet warnings) throws SAXException;
-}
\ No newline at end of file
+}
diff --git a/core/src/main/java/info/openrocket/core/logging/Message.java b/core/src/main/java/info/openrocket/core/logging/Message.java
index 5f97fa01c..e7b9b46a1 100644
--- a/core/src/main/java/info/openrocket/core/logging/Message.java
+++ b/core/src/main/java/info/openrocket/core/logging/Message.java
@@ -1,17 +1,30 @@
package info.openrocket.core.logging;
-import info.openrocket.core.rocketcomponent.RocketComponent;
import java.util.Arrays;
+import java.util.UUID;
+
+import info.openrocket.core.rocketcomponent.RocketComponent;
/**
* Baseclass for logging messages (warnings, errors...)
*/
public abstract class Message implements Cloneable {
+ /** Message ID **/
+ UUID id;
+
/** The rocket component(s) that are the source of this message **/
private RocketComponent[] sources = null;
private MessagePriority priority = MessagePriority.NORMAL;
+ protected Message() {
+ this.id = UUID.randomUUID();
+ }
+
+ protected Message(UUID id) {
+ this.id = id;
+ }
+
/**
* Returns the message text + message source objects.
* @return the message text + message source objects.
@@ -54,6 +67,20 @@ public abstract class Message implements Cloneable {
*/
public abstract boolean replaceBy(Message other);
+ /**
+ * Return the ID
+ */
+ public UUID getID() {
+ return id;
+ }
+
+ /**
+ * Set the ID
+ **/
+ public void setID(UUID id) {
+ this.id = id;
+ }
+
/**
* Return the rocket component(s) that are the source of this warning.
* @return the rocket component(s) that are the source of this warning. Returns null if no sources are specified.
diff --git a/core/src/main/java/info/openrocket/core/logging/MessageSet.java b/core/src/main/java/info/openrocket/core/logging/MessageSet.java
index be11b51d7..f71c8c039 100644
--- a/core/src/main/java/info/openrocket/core/logging/MessageSet.java
+++ b/core/src/main/java/info/openrocket/core/logging/MessageSet.java
@@ -1,5 +1,10 @@
package info.openrocket.core.logging;
+import java.util.AbstractSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
+
import info.openrocket.core.rocketcomponent.RocketComponent;
import info.openrocket.core.util.ArrayList;
import info.openrocket.core.util.BugException;
@@ -7,10 +12,6 @@ import info.openrocket.core.util.ModID;
import info.openrocket.core.util.Monitorable;
import info.openrocket.core.util.Mutable;
-import java.util.AbstractSet;
-import java.util.Iterator;
-import java.util.List;
-
/**
* A set that contains multiple Message
s. When adding a
* {@link Message} to this set, the contents is checked for a message of the
@@ -69,7 +70,7 @@ public abstract class MessageSet extends AbstractSet imple
* @param sources the sources of the message (rocket components that caused the message)
*
*/
- public boolean add(E m, RocketComponent... sources) {
+ public boolean add(E m, RocketComponent... sources) {
mutable.check();
try {
m = (E) m.clone();
@@ -78,7 +79,7 @@ public abstract class MessageSet extends AbstractSet imple
}
m.setSources(sources);
return add(m);
- }
+ }
/**
* Add a Message
of the specified type with the specified discriminator to the
@@ -149,6 +150,14 @@ public abstract class MessageSet extends AbstractSet imple
return list;
}
+ public Message findById(UUID id) {
+ for (Message m : messages) {
+ if (m.id.equals(id))
+ return m;
+ }
+ throw new BugException("Message with id " + id + " not found");
+ }
+
public void immute() {
mutable.immute();
}
diff --git a/core/src/main/java/info/openrocket/core/logging/Warning.java b/core/src/main/java/info/openrocket/core/logging/Warning.java
index 30bd9364b..7361f7180 100644
--- a/core/src/main/java/info/openrocket/core/logging/Warning.java
+++ b/core/src/main/java/info/openrocket/core/logging/Warning.java
@@ -18,7 +18,8 @@ public abstract class Warning extends Message {
* @return a Message with the specific text and priority.
*/
public static Warning fromString(String text, MessagePriority priority) {
- return new Warning.Other(text, priority);
+ Warning.Other warn = new Warning.Other(text, priority);
+ return warn;
}
/**
@@ -27,8 +28,6 @@ public abstract class Warning extends Message {
public static Warning fromString(String text) {
return fromString(text, MessagePriority.NORMAL);
}
-
-
///////////// Specific warning classes /////////////
diff --git a/core/src/main/java/info/openrocket/core/simulation/BasicEventSimulationEngine.java b/core/src/main/java/info/openrocket/core/simulation/BasicEventSimulationEngine.java
index 0e68c559f..97dd2b334 100644
--- a/core/src/main/java/info/openrocket/core/simulation/BasicEventSimulationEngine.java
+++ b/core/src/main/java/info/openrocket/core/simulation/BasicEventSimulationEngine.java
@@ -108,7 +108,7 @@ public class BasicEventSimulationEngine implements SimulationEngine {
// No recovery device
if (!simulationConfig.hasRecoveryDevice()) {
- currentStatus.getWarnings().add(Warning.NO_RECOVERY_DEVICE);
+ currentStatus.addWarning(Warning.NO_RECOVERY_DEVICE);
}
currentStatus.getEventQueue().add(new FlightEvent(FlightEvent.Type.LAUNCH, 0, simulationConditions.getRocket()));
@@ -315,7 +315,7 @@ public class BasicEventSimulationEngine implements SimulationEngine {
if (currentStatus.isLanded() &&
(event.getType() != FlightEvent.Type.ALTITUDE) &&
(event.getType() != FlightEvent.Type.SIMULATION_END))
- currentStatus.getWarnings().add(new Warning.EventAfterLanding(event));
+ currentStatus.addWarning(new Warning.EventAfterLanding(event));
// Check for motor ignition events, add ignition events to queue
for (MotorClusterState state : currentStatus.getActiveMotors() ){
@@ -500,12 +500,12 @@ public class BasicEventSimulationEngine implements SimulationEngine {
}
}
if (numActiveBelow != 1) {
- currentStatus.getWarnings().add(Warning.SEPARATION_ORDER);
+ currentStatus.addWarning(Warning.SEPARATION_ORDER);
}
// If I haven't cleared the rail yet, flag a warning
if (!currentStatus.isLaunchRodCleared()) {
- currentStatus.getWarnings().add(Warning.EARLY_SEPARATION);
+ currentStatus.addWarning(Warning.EARLY_SEPARATION);
}
// Create a new simulation branch for the booster
@@ -565,12 +565,12 @@ public class BasicEventSimulationEngine implements SimulationEngine {
// Check for launch rod
if (!currentStatus.isLaunchRodCleared()) {
- currentStatus.getWarnings().add(Warning.RECOVERY_LAUNCH_ROD);
+ currentStatus.addWarning(Warning.RECOVERY_LAUNCH_ROD);
}
// Check current velocity
if (currentStatus.getRocketVelocity().length() > 20) {
- currentStatus.getWarnings().add(new Warning.HighSpeedDeployment(currentStatus.getRocketVelocity().length()));
+ currentStatus.addWarning(new Warning.HighSpeedDeployment(currentStatus.getRocketVelocity().length()));
}
currentStatus.setLiftoff(true);
diff --git a/core/src/main/java/info/openrocket/core/simulation/FlightEvent.java b/core/src/main/java/info/openrocket/core/simulation/FlightEvent.java
index 3e7f80b49..37b703f5a 100644
--- a/core/src/main/java/info/openrocket/core/simulation/FlightEvent.java
+++ b/core/src/main/java/info/openrocket/core/simulation/FlightEvent.java
@@ -2,6 +2,7 @@ package info.openrocket.core.simulation;
import info.openrocket.core.l10n.Translator;
import info.openrocket.core.logging.SimulationAbort;
+import info.openrocket.core.logging.Warning;
import info.openrocket.core.rocketcomponent.AxialStage;
import info.openrocket.core.rocketcomponent.MotorMount;
import info.openrocket.core.rocketcomponent.RocketComponent;
@@ -81,6 +82,11 @@ public class FlightEvent implements Comparable {
*/
TUMBLE(trans.get("FlightEvent.Type.TUMBLE")),
+ /**
+ * A warning was raised during the execution of the simulation
+ */
+ SIM_WARN(trans.get("FlightEvent.Type.SIM_WARN")),
+
/**
* It is impossible for the simulation proceed due to characteristics
* of the rocket or flight configuration
@@ -118,8 +124,8 @@ public class FlightEvent implements Comparable {
this(type, time, source, null);
}
- public FlightEvent(final FlightEvent _sourceEvent, final RocketComponent _comp, final Object _data) {
- this(_sourceEvent.type, _sourceEvent.time, _comp, _data);
+ public FlightEvent( final FlightEvent sourceEvent, final RocketComponent source, final Object data) {
+ this(sourceEvent.type, sourceEvent.time, source, data);
}
public FlightEvent( final Type type, final double time, final RocketComponent source, final Object data) {
@@ -177,6 +183,13 @@ public class FlightEvent implements Comparable {
// finally, sort on event type
return this.type.ordinal() - o.type.ordinal();
}
+
+ public boolean equals(FlightEvent o) {
+ if ((this.type == Type.SIM_WARN) && (o.type == Type.SIM_WARN))
+ return ((Warning)(this.data)).equals((Warning)(o.data));
+
+ return this.equals(0);
+ }
@Override
public String toString() {
@@ -241,6 +254,17 @@ public class FlightEvent implements Comparable {
}
}
break;
+ case SIM_WARN:
+ if (null != this.source) {
+ // rather than making event sources take sets of components, or trying to keep them
+ // in sync with the sources of Warnings, we'll require the event source to be null
+ // and pull the actual sources from the Warning
+ throw new IllegalStateException(type.name()+" event requires null source component; was " + this.source);
+ }
+ if (( null == this.data ) || ( ! ( this.data instanceof Warning ))) {
+ throw new IllegalStateException(type.name()+" events require Warning objects");
+ }
+ break;
case SIM_ABORT:
if (( null == this.data ) || ( ! ( this.data instanceof SimulationAbort ))) {
throw new IllegalStateException(type.name()+" events require SimulationAbort objects");
diff --git a/core/src/main/java/info/openrocket/core/simulation/RK4SimulationStepper.java b/core/src/main/java/info/openrocket/core/simulation/RK4SimulationStepper.java
index f94746090..b37733e48 100644
--- a/core/src/main/java/info/openrocket/core/simulation/RK4SimulationStepper.java
+++ b/core/src/main/java/info/openrocket/core/simulation/RK4SimulationStepper.java
@@ -401,7 +401,7 @@ public class RK4SimulationStepper extends AbstractSimulationStepper {
* launch rod or 0.25 seconds after departure, and when the velocity has dropped
* below 20% of the max. velocity.
*/
- WarningSet warnings = status.getWarnings();
+ WarningSet warnings = new WarningSet();
store.maxZvelocity = MathUtil.max(store.maxZvelocity, status.getRocketVelocity().z);
if (!status.isLaunchRodCleared()) {
@@ -421,7 +421,9 @@ public class RK4SimulationStepper extends AbstractSimulationStepper {
// Calculate aerodynamic forces
store.forces = status.getSimulationConditions().getAerodynamicCalculator()
.getAerodynamicForces(status.getConfiguration(), store.flightConditions, warnings);
-
+ if (null != warnings) {
+ status.addWarnings(warnings);
+ }
// Add very small randomization to yaw & pitch moments to prevent over-perfect flight
// TODO: HIGH: This should rather be performed as a listener
diff --git a/core/src/main/java/info/openrocket/core/simulation/SimulationStatus.java b/core/src/main/java/info/openrocket/core/simulation/SimulationStatus.java
index 4768bc3d5..54c279bfc 100644
--- a/core/src/main/java/info/openrocket/core/simulation/SimulationStatus.java
+++ b/core/src/main/java/info/openrocket/core/simulation/SimulationStatus.java
@@ -9,6 +9,7 @@ import java.util.Set;
import info.openrocket.core.aerodynamics.FlightConditions;
import info.openrocket.core.logging.SimulationAbort;
+import info.openrocket.core.logging.Warning;
import info.openrocket.core.logging.WarningSet;
import info.openrocket.core.motor.MotorConfiguration;
import info.openrocket.core.motor.MotorConfigurationId;
@@ -418,6 +419,23 @@ public class SimulationStatus implements Cloneable, Monitorable {
this.warnings = warnings;
}
+ public void addWarning(Warning warning) {
+ if (null == warnings) {
+ setWarnings(new WarningSet());
+ }
+ if (!warnings.contains(warning)) {
+ log.trace("Add warning: \"" + warning + "\"");
+ getFlightDataBranch().addEvent(new FlightEvent(FlightEvent.Type.SIM_WARN, getSimulationTime(), null, warning));
+ warnings.add(warning);
+ }
+ }
+
+ public void addWarnings(WarningSet warnings) {
+ for (Warning warning : warnings) {
+ addWarning(warning);
+ }
+ }
+
public WarningSet getWarnings() {
return warnings;
}
diff --git a/core/src/main/java/info/openrocket/core/simulation/listeners/SimulationListenerHelper.java b/core/src/main/java/info/openrocket/core/simulation/listeners/SimulationListenerHelper.java
index e328d2871..ffd6119af 100644
--- a/core/src/main/java/info/openrocket/core/simulation/listeners/SimulationListenerHelper.java
+++ b/core/src/main/java/info/openrocket/core/simulation/listeners/SimulationListenerHelper.java
@@ -637,7 +637,7 @@ public class SimulationListenerHelper {
private static void warn(SimulationStatus status, SimulationListener listener) {
if (!listener.isSystemListener()) {
log.info("Non-system listener " + listener + " affected the simulation");
- status.getWarnings().add(Warning.LISTENERS_AFFECTED);
+ status.addWarning(Warning.LISTENERS_AFFECTED);
}
}
}
diff --git a/core/src/main/resources/l10n/messages.properties b/core/src/main/resources/l10n/messages.properties
index 272459b40..091c418a1 100644
--- a/core/src/main/resources/l10n/messages.properties
+++ b/core/src/main/resources/l10n/messages.properties
@@ -2141,6 +2141,7 @@ FlightEvent.Type.STAGE_SEPARATION = Stage separation
FlightEvent.Type.APOGEE = Apogee
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = Recovery device deployment
FlightEvent.Type.GROUND_HIT = Ground hit
+FlightEvent.Type.SIM_WARN = Warning
FlightEvent.Type.SIM_ABORT = Simulation abort
FlightEvent.Type.SIMULATION_END = Simulation end
FlightEvent.Type.ALTITUDE = Altitude change
diff --git a/core/src/main/resources/l10n/messages_ar.properties b/core/src/main/resources/l10n/messages_ar.properties
index ca25e2b89..609c91090 100644
--- a/core/src/main/resources/l10n/messages_ar.properties
+++ b/core/src/main/resources/l10n/messages_ar.properties
@@ -1711,6 +1711,7 @@ FlightEvent.Type.STAGE_SEPARATION = فصل المرحلة
FlightEvent.Type.APOGEE = الأوج
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = نشر جهاز الإسترداد
FlightEvent.Type.GROUND_HIT = الإصطدام بالأرض
+FlightEvent.Type.SIM_WARN = تحذير
FlightEvent.Type.SIMULATION_END = نهاية المحاكاة
FlightEvent.Type.ALTITUDE = تغيير الإرتفاع
FlightEvent.Type.TUMBLE = هبوط
diff --git a/core/src/main/resources/l10n/messages_de.properties b/core/src/main/resources/l10n/messages_de.properties
index 9499d643c..9544e29a4 100644
--- a/core/src/main/resources/l10n/messages_de.properties
+++ b/core/src/main/resources/l10n/messages_de.properties
@@ -1265,6 +1265,7 @@ FlightEvent.Type.STAGE_SEPARATION = Stufentrennung
FlightEvent.Type.APOGEE = Apogum
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = Auslsung des Bergungssystems
FlightEvent.Type.GROUND_HIT = Landung
+FlightEvent.Type.SIM_WARN = Warnung
FlightEvent.Type.SIMULATION_END = Ende der Simulation
FlightEvent.Type.ALTITUDE = Hhennderung
diff --git a/core/src/main/resources/l10n/messages_es.properties b/core/src/main/resources/l10n/messages_es.properties
index 61fedf408..154d5a816 100644
--- a/core/src/main/resources/l10n/messages_es.properties
+++ b/core/src/main/resources/l10n/messages_es.properties
@@ -350,6 +350,7 @@ FlightEvent.Type.LAUNCH = Lanzamiento
FlightEvent.Type.LAUNCHROD = Abandono de la Gu\u00eda de lanzamiento
FlightEvent.Type.LIFTOFF = Despegue
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = Despliegue del sistema de recuperaci\u00f3n
+FlightEvent.Type.SIM_WARN = Peligro, cohete inestable
FlightEvent.Type.SIMULATION_END = Fin de la simulaci\u00f3n
FlightEvent.Type.STAGE_SEPARATION = Separaci\u00f3n de etapa
FlightEvent.Type.TUMBLE = Volteo
diff --git a/core/src/main/resources/l10n/messages_fr.properties b/core/src/main/resources/l10n/messages_fr.properties
index 9b3e50229..98ca53516 100644
--- a/core/src/main/resources/l10n/messages_fr.properties
+++ b/core/src/main/resources/l10n/messages_fr.properties
@@ -341,6 +341,7 @@ FlightEvent.Type.LAUNCH = Lancement
FlightEvent.Type.LAUNCHROD = D\u00E9gagement de rampe de lancement
FlightEvent.Type.LIFTOFF = D\u00E9collage
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = D\u00E9ploiement du dispositif de r\u00E9cup\u00E9ration
+FlightEvent.Type.SIM_WARN = Avertissement
FlightEvent.Type.SIMULATION_END = Fin de la simulation
FlightEvent.Type.STAGE_SEPARATION = S\u00E9paration d'\u00E9tage
FlightEvent.Type.TUMBLE = D\u00E9gringolade
diff --git a/core/src/main/resources/l10n/messages_it.properties b/core/src/main/resources/l10n/messages_it.properties
index 950a6e7ce..19f4f4715 100644
--- a/core/src/main/resources/l10n/messages_it.properties
+++ b/core/src/main/resources/l10n/messages_it.properties
@@ -1269,6 +1269,7 @@ FlightEvent.Type.STAGE_SEPARATION = Separazione degli stadi
FlightEvent.Type.APOGEE = Apogeo
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = distacco del dispositivo di recupero
FlightEvent.Type.GROUND_HIT = Atterraggio
+FlightEvent.Type,SIM_WARN = Avvisi
FlightEvent.Type.SIMULATION_END = Fine simulazione
FlightEvent.Type.ALTITUDE = Cambio altitudine
diff --git a/core/src/main/resources/l10n/messages_ja.properties b/core/src/main/resources/l10n/messages_ja.properties
index b1e5c1c7c..86e9a7789 100644
--- a/core/src/main/resources/l10n/messages_ja.properties
+++ b/core/src/main/resources/l10n/messages_ja.properties
@@ -1316,6 +1316,7 @@ FlightEvent.Type.STAGE_SEPARATION = \u30B9\u30C6\u30FC\u30B8\u5206\u96E2
FlightEvent.Type.APOGEE = \u6700\u9AD8\u5230\u9054\u70B9
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = \u30EA\u30AB\u30D0\u30EA\u30FC\u88C5\u7F6E\u5C55\u958B
FlightEvent.Type.GROUND_HIT = \u7740\u5730
+FlightEvent.Type.SIM_WARN = \u30A8\u30E9\u30FC
FlightEvent.Type.SIMULATION_END = \u30B7\u30DF\u30E5\u30EC\u30FC\u30B7\u30E7\u30F3\u7D42\u4E86
FlightEvent.Type.ALTITUDE = \u59FF\u52E2\u5909\u66F4
diff --git a/core/src/main/resources/l10n/messages_nl.properties b/core/src/main/resources/l10n/messages_nl.properties
index 690af23a8..ecd1496c5 100644
--- a/core/src/main/resources/l10n/messages_nl.properties
+++ b/core/src/main/resources/l10n/messages_nl.properties
@@ -1634,6 +1634,7 @@ FlightEvent.Type.STAGE_SEPARATION = Trap afscheiding
FlightEvent.Type.APOGEE = Apogee
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = Terugvordering toestel uitrol
FlightEvent.Type.GROUND_HIT = Grond geraakt
+FlightEvent.Type.SIM_WARN = Waarschuwing
FlightEvent.Type.SIMULATION_END = Simulatie-einde
FlightEvent.Type.ALTITUDE = Hoogteverandering
FlightEvent.Type.TUMBLE = Tuimelen
diff --git a/core/src/main/resources/l10n/messages_pl.properties b/core/src/main/resources/l10n/messages_pl.properties
index 4428b15fd..d46226ee5 100644
--- a/core/src/main/resources/l10n/messages_pl.properties
+++ b/core/src/main/resources/l10n/messages_pl.properties
@@ -1210,6 +1210,7 @@ ComponentInfo.EngineBlock = Blokada silnika unieruchamia silnik wewn\u01
FlightEvent.Type.APOGEE = Apogeum
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = Aktywacja uk\u0142adu odzysku
FlightEvent.Type.GROUND_HIT = Uderzenie w ziemi\u0119
+ FlightEvent.Type.SIM_WARN = Ostrze\u017Cenie
FlightEvent.Type.SIMULATION_END = Koniec symulacji
FlightEvent.Type.ALTITUDE = Zmiana wysoko\u015Bci
diff --git a/core/src/main/resources/l10n/messages_pt.properties b/core/src/main/resources/l10n/messages_pt.properties
index 697c732d3..c71236ce9 100644
--- a/core/src/main/resources/l10n/messages_pt.properties
+++ b/core/src/main/resources/l10n/messages_pt.properties
@@ -330,6 +330,7 @@ FlightEvent.Type.LAUNCH = Lan\u00e7amento
FlightEvent.Type.LAUNCHROD = Folga da haste de lan\u00e7amento
FlightEvent.Type.LIFTOFF = Decolagem
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = Implanta\u00e7\u00e3o de dispositivos de recupera\u00e7\u00e3o
+FlightEvent.Type.SIM_WARN = Alerta
FlightEvent.Type.SIMULATION_END = Final de simula\u00e7\u00e3o
FlightEvent.Type.STAGE_SEPARATION = Separa\u00e7\u00e3o do est\u00e1gio
FlightEvent.Type.TUMBLE = Tumbling
diff --git a/core/src/main/resources/l10n/messages_ru.properties b/core/src/main/resources/l10n/messages_ru.properties
index 6bd2670b3..03e9250f1 100644
--- a/core/src/main/resources/l10n/messages_ru.properties
+++ b/core/src/main/resources/l10n/messages_ru.properties
@@ -1683,6 +1683,7 @@ FlightEvent.Type.STAGE_SEPARATION = \u0420\u0430\u0437\u0434\u0435\u043B\u0435\u
FlightEvent.Type.APOGEE = \u0410\u043F\u043E\u0433\u0435\u0439
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = \u0421\u0440\u0430\u0431\u0430\u0442\u044B\u0432\u0430\u043D\u0438\u0435 \u0441\u0438\u0441\u0442\u0435\u043C\u044B \u0441\u043F\u0430\u0441\u0435\u043D\u0438\u044F
FlightEvent.Type.GROUND_HIT = \u041F\u0440\u0438\u0437\u0435\u043C\u043B\u0435\u043D\u0438\u0435
+FlightEvent.Type.SIM_WARN = \u041F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u0435
FlightEvent.Type.SIMULATION_END = \u041A\u043E\u043D\u0435\u0446 \u0440\u0430\u0441\u0447\u0435\u0442\u0430
FlightEvent.Type.ALTITUDE = \u0418\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0435 \u0432\u044B\u0441\u043E\u0442\u044B
FlightEvent.Type.TUMBLE = \u041E\u043F\u0440\u043E\u043A\u0438\u0434\u044B\u0432\u0430\u043D\u0438\u0435
diff --git a/core/src/main/resources/l10n/messages_uk_UA.properties b/core/src/main/resources/l10n/messages_uk_UA.properties
index e3a6b2714..c3e5770ec 100644
--- a/core/src/main/resources/l10n/messages_uk_UA.properties
+++ b/core/src/main/resources/l10n/messages_uk_UA.properties
@@ -2007,6 +2007,7 @@ FlightEvent.Type.SIMULATION_END = \u0417\u0430\u043a\u0456\u043d\u0447\u0435\u04
FlightEvent.Type.ALTITUDE = \u0417\u043c\u0456\u043d\u0430 \u0432\u0438\u0441\u043e\u0442\u0438
FlightEvent.Type.TUMBLE = \u041f\u0435\u0440\u0435\u043a\u0438\u0434\u0430\u043d\u043d\u044f
FlightEvent.Type.EXCEPTION = \u0412\u0438\u043d\u044f\u0442\u043e\u043a
+>>>>>>> unstable
! ThrustCurveMotorColumns
TCurveMotorCol.MANUFACTURER = \u0412\u0438\u0440\u043e\u0431\u043d\u0438\u043a
diff --git a/core/src/main/resources/l10n/messages_zh_CN.properties b/core/src/main/resources/l10n/messages_zh_CN.properties
index 49efc0121..f9903be6f 100644
--- a/core/src/main/resources/l10n/messages_zh_CN.properties
+++ b/core/src/main/resources/l10n/messages_zh_CN.properties
@@ -367,6 +367,7 @@ FlightEvent.Type.LAUNCH = \u53D1\u5C04
FlightEvent.Type.LAUNCHROD = \u79BB\u67B6
FlightEvent.Type.LIFTOFF = \u8D77\u98DE
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = \u56DE\u6536\u88C5\u7F6E\u542F\u52A8
+FlightEvent.Type.SIM_WARN = \u8B66\u544A
FlightEvent.Type.SIMULATION_END = \u4EFF\u771F\u7ED3\u675F
FlightEvent.Type.STAGE_SEPARATION = \u7EA7\u95F4\u5206\u79BB
FlightEvent.Type.TUMBLE = \u7FFB\u6EDA
diff --git a/core/src/test/java/info/openrocket/core/simulation/FlightEventsTest.java b/core/src/test/java/info/openrocket/core/simulation/FlightEventsTest.java
index fa6a893f3..3654f5eee 100644
--- a/core/src/test/java/info/openrocket/core/simulation/FlightEventsTest.java
+++ b/core/src/test/java/info/openrocket/core/simulation/FlightEventsTest.java
@@ -1,6 +1,12 @@
package info.openrocket.core.simulation;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
import info.openrocket.core.document.Simulation;
+import info.openrocket.core.logging.Warning;
import info.openrocket.core.rocketcomponent.AxialStage;
import info.openrocket.core.rocketcomponent.BodyTube;
import info.openrocket.core.rocketcomponent.FlightConfigurationId;
@@ -11,15 +17,12 @@ import info.openrocket.core.rocketcomponent.Rocket;
import info.openrocket.core.simulation.exception.SimulationException;
import info.openrocket.core.util.BaseTestCase;
import info.openrocket.core.util.TestRockets;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertSame;;
/**
* Tests to verify that simulations contain all the expected flight events.
*/
public class FlightEventsTest extends BaseTestCase {
+
private static final double EPSILON = 0.005;
/**
@@ -50,6 +53,7 @@ public class FlightEventsTest extends BaseTestCase {
new FlightEvent(FlightEvent.Type.LAUNCHROD, 0.13, null),
new FlightEvent(FlightEvent.Type.BURNOUT, 2.0, motorMountTube),
new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 2.0, stage),
+ new FlightEvent(FlightEvent.Type.SIM_WARN, 2.0, null, new Warning.HighSpeedDeployment(80.6)),
new FlightEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, 2.001, parachute),
new FlightEvent(FlightEvent.Type.APOGEE, 2.48, rocket),
new FlightEvent(FlightEvent.Type.GROUND_HIT, 42.97, null),
@@ -99,6 +103,9 @@ public class FlightEventsTest extends BaseTestCase {
final ParallelStage sideBoosters = (ParallelStage) centerBoosterBody.getChild(1);
final BodyTube sideBoosterBodies = (BodyTube) sideBoosters.getChild(1);
+ Warning warn = Warning.OPEN_AIRFRAME_FORWARD;
+ warn.setSources(new BodyTube[]{centerBoosterBody});
+
// events whose time is too variable to check are given a time of 1200
for (int b = 0; b < 2; b++) {
FlightEvent[] expectedEvents = switch (b) {
@@ -130,6 +137,7 @@ public class FlightEventsTest extends BaseTestCase {
new FlightEvent(FlightEvent.Type.BURNOUT, 2.01, centerBoosterBody),
new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 2.01, centerBooster),
new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 2.01, centerBooster),
+ new FlightEvent(FlightEvent.Type.SIM_WARN, 2.01, null, warn),
new FlightEvent(FlightEvent.Type.TUMBLE, 2.85, null),
new FlightEvent(FlightEvent.Type.APOGEE, 3.78, rocket),
new FlightEvent(FlightEvent.Type.GROUND_HIT, 9.0, null),
@@ -158,12 +166,9 @@ public class FlightEventsTest extends BaseTestCase {
FlightEvent[] actualEvents = sim.getSimulatedData().getBranch(branchNo).getEvents().toArray(new FlightEvent[0]);
- // Test event count
- assertEquals(expectedEvents.length, actualEvents.length, "Branch " + branchNo + " invalid number of events ");
-
// Test that all expected events are present, in the right order, at the right
// time, from the right sources
- for (int i = 0; i < actualEvents.length; i++) {
+ for (int i = 0; i < Math.min(expectedEvents.length, actualEvents.length); i++) {
final FlightEvent expected = expectedEvents[i];
final FlightEvent actual = actualEvents[i];
assertSame(expected.getType(), actual.getType(),
@@ -185,6 +190,16 @@ public class FlightEventsTest extends BaseTestCase {
// Test that the event sources are correct
assertEquals(expected.getSource(), actual.getSource(),
"Branch " + branchNo + " FlightEvent " + i + " type " + expected.getType() + " has wrong source ");
+
+ // If it's a warning event, make sure the warning types match
+ if (expected.getType() == FlightEvent.Type.SIM_WARN) {
+ assertTrue(actual.getData() instanceof Warning, "SIM_WARN event data is not a Warning");
+ assertTrue(((Warning) expected.getData()).equals(actual.getData()), "Expected: " + expected.getData() + " but was: " + actual.getData());
+ }
}
+
+ // Test event count
+ assertEquals(expectedEvents.length, actualEvents.length, "Branch " + branchNo + " incorrect number of events ");
+
}
}
diff --git a/fileformat.txt b/fileformat.txt
index 144751d0b..f6503c444 100644
--- a/fileformat.txt
+++ b/fileformat.txt
@@ -71,3 +71,5 @@ The following file format versions exist:
Added a priority attribute to simulation warnings.
Added document preferences ().
Added wind model settings (), and windmodeltype to simulation conditions.
+ Added warning flight events
+
diff --git a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlot.java b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlot.java
index ba98485dd..5ae60db0b 100644
--- a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlot.java
+++ b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlot.java
@@ -26,7 +26,7 @@ public class CAPlot extends Plot
// Create the series for each component
List allSeries = new ArrayList<>();
for (int i = 0; i < components.size(); i++) {
- XYSeries series = createSingleSeries(startIndex*1000 + i, type, unit, branch, branchIdx, branchName, baseName,
+ XYSeries series = createSingleSeries(startIndex*1000 + i, type, unit, branch, branchIdx, branchName, dataIndex, baseName,
components.get(i), componentNames.get(i));
allSeries.add(series);
}
@@ -35,10 +35,10 @@ public class CAPlot extends Plot
}
private XYSeries createSingleSeries(int key, CADataType type, Unit unit,
- CADataBranch branch, int branchIdx, String branchName, String baseName,
+ CADataBranch branch, int branchIdx, String branchName, int dataIndex, String baseName,
RocketComponent component, String componentName) {
// Default implementation for regular DataBranch
- MetadataXYSeries series = new MetadataXYSeries(key, false, true, branchIdx, unit.getUnit(),
+ MetadataXYSeries series = new MetadataXYSeries(key, false, true, branchIdx, dataIndex, unit.getUnit(),
branchName, baseName);
// Create a new description that includes the component name
diff --git a/swing/src/main/java/info/openrocket/swing/gui/plot/EventGraphics.java b/swing/src/main/java/info/openrocket/swing/gui/plot/EventGraphics.java
index 06911c9f1..27005f4da 100644
--- a/swing/src/main/java/info/openrocket/swing/gui/plot/EventGraphics.java
+++ b/swing/src/main/java/info/openrocket/swing/gui/plot/EventGraphics.java
@@ -9,20 +9,27 @@ import java.util.Map;
import javax.imageio.ImageIO;
+import info.openrocket.core.logging.MessagePriority;
+import info.openrocket.core.logging.Warning;
import info.openrocket.core.simulation.FlightEvent;
public class EventGraphics {
- static Color getEventColor(FlightEvent.Type type) {
+ static Color getEventColor(FlightEvent event) {
+ FlightEvent.Type type = event.getType();
Color c = EVENT_COLORS.get(type);
if (c != null)
return c;
return DEFAULT_EVENT_COLOR;
}
- static Image getEventImage(FlightEvent.Type type ) {
- Image i = EVENT_IMAGES.get(type);
- return i;
+ static Image getEventImage(FlightEvent event) {
+ FlightEvent.Type type = event.getType();
+ if (type == FlightEvent.Type.SIM_WARN) {
+ return MESSAGE_IMAGES.get(((Warning) event.getData()).getPriority());
+ } else {
+ return EVENT_IMAGES.get(type);
+ }
}
private static final Color DEFAULT_EVENT_COLOR = new Color(0, 0, 0);
@@ -41,29 +48,38 @@ public class EventGraphics {
EVENT_COLORS.put(FlightEvent.Type.SIMULATION_END, new Color(128, 0, 0));
EVENT_COLORS.put(FlightEvent.Type.TUMBLE, new Color(196, 0, 255));
EVENT_COLORS.put(FlightEvent.Type.EXCEPTION, new Color(255, 0, 0));
+ EVENT_COLORS.put(FlightEvent.Type.SIM_WARN, new Color(127, 127, 0));
EVENT_COLORS.put(FlightEvent.Type.SIM_ABORT, new Color(255, 0, 0));
}
private static final Map EVENT_IMAGES = new HashMap<>();
static {
- loadImage(FlightEvent.Type.LAUNCH, "pix/eventicons/event-launch.png");
- loadImage(FlightEvent.Type.LIFTOFF, "pix/eventicons/event-liftoff.png");
- loadImage(FlightEvent.Type.LAUNCHROD, "pix/eventicons/event-launchrod.png");
- loadImage(FlightEvent.Type.IGNITION, "pix/eventicons/event-ignition.png");
- loadImage(FlightEvent.Type.BURNOUT, "pix/eventicons/event-burnout.png");
- loadImage(FlightEvent.Type.EJECTION_CHARGE, "pix/eventicons/event-ejection-charge.png");
- loadImage(FlightEvent.Type.STAGE_SEPARATION,
+ loadImage(EVENT_IMAGES, FlightEvent.Type.LAUNCH, "pix/eventicons/event-launch.png");
+ loadImage(EVENT_IMAGES, FlightEvent.Type.LIFTOFF, "pix/eventicons/event-liftoff.png");
+ loadImage(EVENT_IMAGES, FlightEvent.Type.LAUNCHROD, "pix/eventicons/event-launchrod.png");
+ loadImage(EVENT_IMAGES, FlightEvent.Type.IGNITION, "pix/eventicons/event-ignition.png");
+ loadImage(EVENT_IMAGES, FlightEvent.Type.BURNOUT, "pix/eventicons/event-burnout.png");
+ loadImage(EVENT_IMAGES, FlightEvent.Type.EJECTION_CHARGE, "pix/eventicons/event-ejection-charge.png");
+ loadImage(EVENT_IMAGES, FlightEvent.Type.STAGE_SEPARATION,
"pix/eventicons/event-stage-separation.png");
- loadImage(FlightEvent.Type.APOGEE, "pix/eventicons/event-apogee.png");
- loadImage(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT,
+ loadImage(EVENT_IMAGES, FlightEvent.Type.APOGEE, "pix/eventicons/event-apogee.png");
+ loadImage(EVENT_IMAGES, FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT,
"pix/eventicons/event-recovery-device-deployment.png");
- loadImage(FlightEvent.Type.GROUND_HIT, "pix/eventicons/event-ground-hit.png");
- loadImage(FlightEvent.Type.SIMULATION_END, "pix/eventicons/event-simulation-end.png");
- loadImage(FlightEvent.Type.EXCEPTION, "pix/eventicons/event-exception.png");
- loadImage(FlightEvent.Type.SIM_ABORT, "pix/eventicons/event-exception.png");
+ loadImage(EVENT_IMAGES, FlightEvent.Type.GROUND_HIT, "pix/eventicons/event-ground-hit.png");
+ loadImage(EVENT_IMAGES, FlightEvent.Type.SIMULATION_END, "pix/eventicons/event-simulation-end.png");
+ loadImage(EVENT_IMAGES, FlightEvent.Type.EXCEPTION, "pix/eventicons/event-exception.png");
+ loadImage(EVENT_IMAGES, FlightEvent.Type.SIM_ABORT, "pix/eventicons/event-exception.png");
+ }
+
+ // Messages can happen at several priority levels, requiring different icons
+ private static final Map MESSAGE_IMAGES = new HashMap();
+ static {
+ loadImage(MESSAGE_IMAGES, MessagePriority.LOW, "pix/icons/warning_low.png");
+ loadImage(MESSAGE_IMAGES, MessagePriority.NORMAL, "pix/icons/warning_normal.png");
+ loadImage(MESSAGE_IMAGES, MessagePriority.HIGH, "pix/icons/warning_high.png");
}
- private static void loadImage(FlightEvent.Type type, String file) {
+ private static void loadImage(Map imageMap, KeyType type, String file) {
InputStream is;
is = ClassLoader.getSystemResourceAsStream(file);
@@ -74,10 +90,9 @@ public class EventGraphics {
try {
Image image = ImageIO.read(is);
- EVENT_IMAGES.put(type, image);
+ imageMap.put(type, image);
} catch (IOException ignore) {
ignore.printStackTrace();
}
}
-
}
diff --git a/swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java b/swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java
index ce9c532c3..7c24334aa 100644
--- a/swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java
+++ b/swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java
@@ -1,8 +1,11 @@
package info.openrocket.swing.gui.plot;
import info.openrocket.core.l10n.Translator;
+import info.openrocket.core.logging.Warning;
import info.openrocket.core.simulation.DataBranch;
import info.openrocket.core.simulation.DataType;
+import info.openrocket.core.simulation.FlightDataType;
+import info.openrocket.core.simulation.FlightEvent;
import info.openrocket.core.startup.Application;
import info.openrocket.core.unit.Unit;
import info.openrocket.core.unit.UnitGroup;
@@ -52,6 +55,7 @@ import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
/*
* TODO: It should be possible to simplify this code quite a bit by using a single Renderer instance for
@@ -69,7 +73,7 @@ public abstract class Plot, C extend
protected final List renderers = new ArrayList<>();
protected final LegendItems legendItems;
protected final XYSeriesCollection[] data;
- protected final C filledConfig; // Configuration after using 'fillAutoAxes'
+ protected final C filledConfig; // Configuration after using 'fillAutoAxes' and 'fitAxes'
protected final JFreeChart chart;
@@ -101,8 +105,8 @@ public abstract class Plot, C extend
// Create the data series for both axes
this.data = new XYSeriesCollection[2];
- this.data[0] = new XYSeriesCollection();
- this.data[1] = new XYSeriesCollection();
+ this.data[Util.PlotAxisSelection.LEFT.getValue()] = new XYSeriesCollection();
+ this.data[Util.PlotAxisSelection.RIGHT.getValue()] = new XYSeriesCollection();
// Fill the auto-selections based on first branch selected.
this.filledConfig = config.fillAutoAxes(mainBranch);
@@ -120,9 +124,8 @@ public abstract class Plot, C extend
int seriesCount = 0;
// Compute the axes based on the min and max value of all branches
- C plotConfig = filledConfig.cloneConfiguration();
- plotConfig.fitAxes(allBranches);
- List minMaxAxes = plotConfig.getAllAxes();
+ filledConfig.fitAxes(allBranches);
+ List minMaxAxes = filledConfig.getAllAxes();
// Create the XYSeries objects from the flight data and store into the collections
String[] axisLabel = new String[2];
@@ -197,21 +200,40 @@ public abstract class Plot, C extend
StandardXYToolTipGenerator tooltipGenerator = new StandardXYToolTipGenerator() {
@Override
public String generateToolTip(XYDataset dataset, int series, int item) {
+
XYSeriesCollection collection = data[finalAxisno];
if (collection.getSeriesCount() == 0) {
return null;
}
MetadataXYSeries ser = (MetadataXYSeries) collection.getSeries(series);
- String unitY = ser.getUnit();
- String unitX = domainUnit.getUnit();
-
- double dataY = dataset.getYValue(series, item);
- double dataX = dataset.getXValue(series, item);
// Determine the appropriate name based on the time and series
String name = getNameBasedOnIdxAndSeries(ser, item);
- return formatSampleTooltip(name, dataX, unitX, dataY, unitY, item);
+ int dataTypeIdx = ser.getDataIdx();
+ DataType type = config.getType(dataTypeIdx);
+
+ String nameT = FlightDataType.TYPE_TIME.getName();
+ double dataT = Double.NaN;
+ List time = allBranches.get(ser.getBranchIdx()).get((T)FlightDataType.TYPE_TIME);
+ if (null != time) {
+ dataT = time.get(item);
+ }
+ String unitT = FlightDataType.TYPE_TIME.getUnitGroup().getDefaultUnit().toString();
+
+ String nameX = config.getDomainAxisType().getName();
+ double dataX = dataset.getXValue(series, item);
+ String unitX = domainUnit.getUnit();
+
+ String nameY = type.toString();
+ double dataY = dataset.getYValue(series, item);
+ String unitY = ser.getUnit();
+
+ return formatTooltip(name,
+ nameT, dataT, unitT,
+ nameX, dataX, unitX,
+ nameY, dataY, unitY,
+ null);
}
};
@@ -278,7 +300,7 @@ public abstract class Plot, C extend
protected List createSeriesForType(int dataIndex, int startIndex, T type, Unit unit, B branch,
int branchIdx, String branchName, String baseName) {
// Default implementation for regular DataBranch
- MetadataXYSeries series = new MetadataXYSeries(startIndex, false, true, branchIdx, unit.getUnit(), branchName, baseName);
+ MetadataXYSeries series = new MetadataXYSeries(startIndex, false, true, branchIdx, dataIndex, unit.getUnit(), branchName, baseName);
List plotx = branch.get(filledConfig.getDomainAxisType());
List ploty = branch.get(type);
@@ -297,44 +319,65 @@ public abstract class Plot, C extend
return type;
}
- protected String formatSampleTooltip(String dataName, double dataX, String unitX, double dataY, String unitY,
- int sampleIdx, boolean addYValue) {
- String ord_end = getOrdinalEnding(sampleIdx);
+ protected String formatTooltip(String dataName,
+ String nameT, double dataT, String unitT,
+ String nameX, double dataX, String unitX,
+ String nameY, double dataY, String unitY,
+ Set events) {
- DecimalFormat df_y = DecimalFormatter.df(dataY, 2, false);
+ final String strFormat = "%s: %s %s
";
+
DecimalFormat df_x = DecimalFormatter.df(dataX, 2, false);
StringBuilder sb = new StringBuilder();
- sb.append(String.format("" +
- "%s
", dataName));
+ sb.append("");
- if (addYValue) {
- sb.append(String.format("Y: %s %s
", df_y.format(dataY), unitY));
+ sb.append(String.format("%s
", dataName));
+
+ // Any events?
+ if ((null != events) && (events.size() != 0)) {
+ // Pass through and collect any warnings
+ for (FlightEvent event : events) {
+ if (event.getType() == FlightEvent.Type.SIM_WARN) {
+ sb.append("Warning: " + ((Warning) event.getData()).toString() + "
");
+ }
+ }
+
+ // Now pass through and collect the other events
+ String eventStr = "";
+ for (FlightEvent event : events) {
+ if (event.getType() != FlightEvent.Type.SIM_WARN) {
+ if (eventStr != "") {
+ eventStr = eventStr + ", ";
+ }
+ eventStr = eventStr + event.getType();
+ }
+ }
+ sb.append(eventStr + "
");
}
- sb.append(String.format("X: %s %s
" +
- "%d%s sample" +
- "", df_x.format(dataX), unitX, sampleIdx, ord_end));
+ // Valid Y data?
+ if (!Double.isNaN(dataY)) {
+ DecimalFormat df_y = DecimalFormatter.df(dataY, 2, false);
+ sb.append(String.format(strFormat, nameY, df_y.format(dataY), unitY));
+ }
+ // Assuming X data is valid
+ sb.append(String.format(strFormat, nameX, df_x.format(dataX), unitX));
+
+ // If I've got time data, and my domain isn't time, add time to tooltip
+ if (!Double.isNaN(dataT) && !nameX.equals(nameT)) {
+ DecimalFormat df_t = DecimalFormatter.df(dataT, 2, false);
+ sb.append(String.format(strFormat, nameT, df_t.format(dataT), unitT));
+ }
+
+ sb.append("");
+
return sb.toString();
}
- protected String formatSampleTooltip(String dataName, double dataX, String unitX, double dataY, String unitY, int sampleIdx) {
- return formatSampleTooltip(dataName, dataX, unitX, dataY, unitY, sampleIdx, true);
- }
-
- protected String formatSampleTooltip(String dataName, double dataX, String unitX, int sampleIdx) {
- return formatSampleTooltip(dataName, dataX, unitX, 0, "", sampleIdx, false);
- }
-
- private String getOrdinalEnding(int n) {
- if (n % 100 == 11 || n % 100 == 12 || n % 100 == 13) return "th";
- return switch (n % 10) {
- case 1 -> "st";
- case 2 -> "nd";
- case 3 -> "rd";
- default -> "th";
- };
+ protected String formatTooltip(String dataName, String nameX, double dataX, String unitX) {
+ return formatTooltip(dataName, "", Double.NaN, "", nameX, dataX, unitX, "", Double.NaN, "", null);
}
protected static class LegendItems implements LegendItemSource {
@@ -540,20 +583,27 @@ public abstract class Plot, C extend
protected static class MetadataXYSeries extends XYSeries {
private final int branchIdx;
+ private final int dataIdx;
private final String unit;
private final String branchName;
private String baseName;
- public MetadataXYSeries(Comparable key, boolean autoSort, boolean allowDuplicateXValues, int branchIdx, String unit,
+ public MetadataXYSeries(Comparable key, boolean autoSort, boolean allowDuplicateXValues, int branchIdx, int dataIdx, String unit,
String branchName, String baseName) {
super(key, autoSort, allowDuplicateXValues);
this.branchIdx = branchIdx;
+ this.dataIdx = dataIdx;
this.unit = unit;
this.branchName = branchName;
this.baseName = baseName;
updateDescription();
}
+ public MetadataXYSeries(Comparable key, boolean autoSort, boolean allowDuplicateXValues, int branchIdx, String unit,
+ String branchName, String baseName) {
+ this(key, autoSort, allowDuplicateXValues, branchIdx, -1, unit, branchName, baseName);
+ }
+
public String getUnit() {
return unit;
}
@@ -562,6 +612,10 @@ public abstract class Plot, C extend
return branchIdx;
}
+ public int getDataIdx() {
+ return dataIdx;
+ }
+
public String getBranchName() {
return branchName;
}
diff --git a/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlot.java b/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlot.java
index 40ff38540..5a9a38031 100644
--- a/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlot.java
+++ b/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlot.java
@@ -5,6 +5,7 @@ import java.awt.Font;
import java.awt.Image;
import java.util.ArrayList;
import java.util.Comparator;
+import java.text.DecimalFormat;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
@@ -13,11 +14,13 @@ import java.util.Set;
import info.openrocket.core.document.Simulation;
import info.openrocket.core.logging.SimulationAbort;
+import info.openrocket.core.logging.Warning;
import info.openrocket.core.simulation.FlightDataBranch;
import info.openrocket.core.simulation.FlightDataType;
import info.openrocket.core.simulation.FlightEvent;
import info.openrocket.core.preferences.ApplicationPreferences;
import info.openrocket.core.util.LinearInterpolator;
+import info.openrocket.swing.utils.DecimalFormatter;
import org.jfree.chart.annotations.XYImageAnnotation;
import org.jfree.chart.annotations.XYTitleAnnotation;
@@ -31,6 +34,8 @@ import org.jfree.chart.ui.VerticalAlignment;
import org.jfree.chart.ui.RectangleAnchor;
import org.jfree.chart.ui.RectangleEdge;
import org.jfree.chart.ui.RectangleInsets;
+import org.jfree.data.xy.XYSeries;
+import org.jfree.data.xy.XYSeriesCollection;
@SuppressWarnings("serial")
public class SimulationPlot extends Plot {
@@ -106,32 +111,31 @@ public class SimulationPlot extends Plot eventTimes = new ArrayList<>();
- List eventLabels = new ArrayList<>();
List eventColors = new ArrayList<>();
List eventImages = new ArrayList<>();
+ List> eventSets = new ArrayList<>();
// Plot the markers
if (config.getDomainAxisType() == FlightDataType.TYPE_TIME && !preferences.getBoolean(ApplicationPreferences.MARKER_STYLE_ICON, false)) {
- fillEventLists(branch, eventTimes, eventLabels, eventColors, eventImages);
- plotVerticalLineMarkers(plot, eventTimes, eventLabels, eventColors);
+ fillEventLists(branch, eventColors, eventImages, eventSets);
+ plotVerticalLineMarkers(plot, eventColors, eventSets);
} else { // Other domains are plotted as image annotations
if (branch == -1) {
// For icon markers, we need to do the plotting separately, otherwise you can have icon markers from e.g.
- // branch 1 be plotted on branch 0
- for (int b = 0; b < simulation.getSimulatedData().getBranchCount(); b++) {
- fillEventLists(b, eventTimes, eventLabels, eventColors, eventImages);
+ // branch 1 be plotted on branch 0. Also, need to take the branches in reverse order so lower number
+ // branches get their icons put on top of higher number and the tooltip headings are correct.
+ for (int b = simulation.getSimulatedData().getBranchCount() - 1; b >= 0; b--) {
+ fillEventLists(b, eventColors, eventImages, eventSets);
dataBranch = simulation.getSimulatedData().getBranch(b);
- plotIconMarkers(plot, dataBranch, eventTimes, eventLabels, eventImages);
- eventTimes.clear();
- eventLabels.clear();
+ plotIconMarkers(plot, simulation, b, eventImages, eventSets);
+ eventSets = new ArrayList<>();
eventColors.clear();
eventImages.clear();
}
} else {
- fillEventLists(branch, eventTimes, eventLabels, eventColors, eventImages);
- plotIconMarkers(plot, dataBranch, eventTimes, eventLabels, eventImages);
+ fillEventLists(branch, eventColors, eventImages, eventSets);
+ plotIconMarkers(plot, simulation, branch, eventImages, eventSets);
}
}
}
@@ -154,78 +158,92 @@ public class SimulationPlot extends Plot dataIdx) {
+ if (separationIdx != -1 && separationIdx >= dataIdx) {
newBranchName.append(" + ").append(allBranches.get(i).getName());
}
}
-
return newBranchName + ": " + ser.getBaseName();
}
- private void fillEventLists(int branch, List eventTimes, List eventLabels,
- List eventColors, List eventImages) {
+ private void fillEventLists(int branch,
+ List eventColors, List eventImages, List> eventSets) {
+ Set eventSet = new HashSet<>();
Set typeSet = new HashSet<>();
double prevTime = -100;
- String text = null;
Color color = null;
Image image = null;
int maxOrdinal = -1;
+
for (EventDisplayInfo info : eventList) {
if (branch >= 0 && branch != info.stage) {
continue;
}
double t = info.time;
- FlightEvent.Type type = info.event.getType();
+ FlightEvent event = info.event;
+ FlightEvent.Type type = event.getType();
+
if (Math.abs(t - prevTime) <= 0.05) {
if (!typeSet.contains(type)) {
- text = text + ", " + type.toString();
if (type.ordinal() > maxOrdinal) {
- color = EventGraphics.getEventColor(type);
- image = EventGraphics.getEventImage(type);
+ color = EventGraphics.getEventColor(event);
+ image = EventGraphics.getEventImage(event);
maxOrdinal = type.ordinal();
}
typeSet.add(type);
+ eventSet.add(event);
}
} else {
- if (text != null) {
- eventTimes.add(prevTime);
- eventLabels.add(text);
+ if (!eventSet.isEmpty()) {
eventColors.add(color);
eventImages.add(image);
+ eventSets.add(eventSet);
}
prevTime = t;
- text = type.toString();
- color = EventGraphics.getEventColor(type);
- image = EventGraphics.getEventImage(type);
+ color = EventGraphics.getEventColor(event);
+ image = EventGraphics.getEventImage(event);
typeSet.clear();
typeSet.add(type);
+ eventSet = new HashSet<>();
+ eventSet.add(event);
maxOrdinal = type.ordinal();
}
-
}
- if (text != null) {
- eventTimes.add(prevTime);
- eventLabels.add(text);
+ if (!eventSet.isEmpty()) {
eventColors.add(color);
eventImages.add(image);
+ eventSets.add(eventSet);
}
}
- private static void plotVerticalLineMarkers(XYPlot plot, List eventTimes, List eventLabels, List eventColors) {
+ private static String constructEventLabels(Set events) {
+ String text = "";
+
+ for (FlightEvent event : events) {
+ if (text != "") {
+ text += ", ";
+ }
+ text += event.getType().toString();
+ }
+
+ return text;
+ }
+
+ private static void plotVerticalLineMarkers(XYPlot plot, List eventColors, List> eventSets) {
double markerWidth = 0.01 * plot.getDomainAxis().getUpperBound();
// Domain time is plotted as vertical lines
- for (int i = 0; i < eventTimes.size(); i++) {
- double t = eventTimes.get(i);
- String event = eventLabels.get(i);
+ for (int i = 0; i < eventSets.size(); i++) {
+ Set events = eventSets.get(i);
+ double t = ((FlightEvent)events.toArray()[0]).getTime();
+ String eventLabel = constructEventLabels(events);
Color color = eventColors.get(i);
ValueMarker m = new ValueMarker(t);
- m.setLabel(event);
+ m.setLabel(eventLabel);
m.setPaint(color);
m.setLabelPaint(color);
m.setAlpha(0.7f);
@@ -238,53 +256,85 @@ public class SimulationPlot extends Plot eventTimes,
- List eventLabels, List eventImages) {
+ private void plotIconMarkers(XYPlot plot, Simulation simulation, int branch, List eventImages, List> eventSets) {
+
+ FlightDataBranch dataBranch = simulation.getSimulatedData().getBranch(branch);
+
List time = dataBranch.get(FlightDataType.TYPE_TIME);
+ String tName = FlightDataType.TYPE_TIME.getName();
List domain = dataBranch.get(config.getDomainAxisType());
-
LinearInterpolator domainInterpolator = new LinearInterpolator(time, domain);
+ String xName = config.getDomainAxisType().getName();
- for (int i = 0; i < eventTimes.size(); i++) {
- double t = eventTimes.get(i);
- Image image = eventImages.get(i);
+ List minMaxAxes = filledConfig.getAllAxes();
- if (image == null) {
- continue;
- }
+ for (int axisno = 0; axisno < data.length; axisno++) {
+ // Image annotations are drawn using the data space defined by the left axis, so
+ // the position of annotations on the right axis need to be mapped to the left axis
+ // dataspace.
+
+ double minLeft = minMaxAxes.get(0).getMinValue();
+ double maxLeft = minMaxAxes.get(0).getMaxValue();
+
+ double minThis = minMaxAxes.get(axisno).getMinValue();
+ double maxThis = minMaxAxes.get(axisno).getMaxValue();
+
+ double slope = (maxLeft - minLeft)/(maxThis - minThis);
+ double intercept = (maxThis * minLeft - maxLeft * minThis)/(maxThis - minThis);
+
+ XYSeriesCollection collection = data[axisno];
+ for (MetadataXYSeries series : (List)(collection.getSeries())) {
- double xcoord = domainInterpolator.getValue(t);
-
- for (int index = 0; index < config.getDataCount(); index++) {
- FlightDataType type = config.getType(index);
- List range = dataBranch.get(type);
-
- LinearInterpolator rangeInterpolator = new LinearInterpolator(time, range);
- // Image annotations are not supported on the right-side axis
- // TODO: LOW: Can this be achieved by JFreeChart?
- if (filledConfig.getAxis(index) != Util.PlotAxisSelection.LEFT.getValue()) {
+ if (series.getBranchIdx() != branch) {
continue;
}
- double ycoord = rangeInterpolator.getValue(t);
- if (!Double.isNaN(ycoord)) {
- // Convert units
+ int dataTypeIdx = series.getDataIdx();
+ FlightDataType type = config.getType(dataTypeIdx);
+ String yName = type.toString();
+ List range = dataBranch.get(type);
+ LinearInterpolator rangeInterpolator = new LinearInterpolator(time, range);
+
+ for (int i = 0; i < eventSets.size(); i++) {
+ Set events = eventSets.get(i);
+ double t = ((FlightEvent)events.toArray()[0]).getTime();
+ Image image = eventImages.get(i);
+ if (image == null) {
+ continue;
+ }
+
+ double xcoord = domainInterpolator.getValue(t);
+ double ycoord = rangeInterpolator.getValue(t);
+
xcoord = config.getDomainAxisUnit().toUnit(xcoord);
- ycoord = config.getUnit(index).toUnit(ycoord);
-
- // Get the sample index of the flight event. Because this can be an interpolation between two samples,
- // take the closest sample.
- final int sampleIdx;
- Optional closestSample = time.stream()
- .min(Comparator.comparingDouble(sample -> Math.abs(sample - t)));
- sampleIdx = closestSample.map(time::indexOf).orElse(-1);
-
- String tooltipText = formatSampleTooltip(eventLabels.get(i), xcoord, config.getDomainAxisUnit().getUnit(), sampleIdx) ;
-
- XYImageAnnotation annotation =
- new XYImageAnnotation(xcoord, ycoord, image, RectangleAnchor.CENTER);
- annotation.setToolTipText(tooltipText);
- plot.addAnnotation(annotation);
+ ycoord = config.getUnit(dataTypeIdx).toUnit(ycoord);
+
+ if (!Double.isNaN(ycoord)) {
+ // Get the sample index of the flight event. Because this can be an interpolation between two samples,
+ // take the closest sample.
+ final int sampleIdx;
+ Optional closestSample = time.stream()
+ .min(Comparator.comparingDouble(sample -> Math.abs(sample - t)));
+ sampleIdx = closestSample.map(time::indexOf).orElse(-1);
+
+ // Convert units
+ String unitX = config.getDomainAxisUnit().getUnit();
+ String unitY = series.getUnit();
+ String unitT = FlightDataType.TYPE_TIME.getUnitGroup().getDefaultUnit().toString();
+ String tooltipText = formatTooltip(getNameBasedOnIdxAndSeries(series, sampleIdx),
+ tName, t, unitT,
+ xName, xcoord, unitX,
+ yName, ycoord, unitY,
+ events);
+ double yloc = slope * ycoord + intercept;
+
+ if (!Double.isNaN(xcoord) && !Double.isNaN(ycoord)) {
+ XYImageAnnotation annotation =
+ new XYImageAnnotation(xcoord, yloc, image, RectangleAnchor.CENTER);
+ annotation.setToolTipText(tooltipText);
+ plot.addAnnotation(annotation);
+ }
+ }
}
}
}
diff --git a/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlotConfiguration.java b/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlotConfiguration.java
index 258ad08cf..cd6abbde2 100644
--- a/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlotConfiguration.java
+++ b/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlotConfiguration.java
@@ -32,6 +32,7 @@ public class SimulationPlotConfiguration extends PlotConfiguration