Merge pull request #2562 from JoePfeiffer/flight-warning-event

Flight warning event
This commit is contained in:
Sibo Van Gool 2024-10-02 22:41:56 +02:00 committed by GitHub
commit 9e83cbe696
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 546 additions and 198 deletions

View File

@ -357,7 +357,7 @@ public class Simulation implements ChangeSource, Cloneable {
public boolean hasErrors() { public boolean hasErrors() {
FlightData data = getSimulatedData(); FlightData data = getSimulatedData();
for (int branchNo = 0; branchNo < data.getBranchCount(); branchNo++) { for (int branchNo = 0; branchNo < data.getBranchCount(); branchNo++) {
if (data.getBranch(branchNo).getFirstEvent(FlightEvent.Type.SIM_ABORT) != null) { if (hasErrors(branchNo)) {
return true; return true;
} }
} }

View File

@ -259,8 +259,12 @@ public class CSVExport {
private static void printEvent(PrintWriter writer, FlightEvent e, private static void printEvent(PrintWriter writer, FlightEvent e,
String commentStarter) { 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")); " 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, private static void writeSimulationComments(PrintWriter writer,

View File

@ -430,7 +430,24 @@ public class OpenRocketSaver extends RocketSaver {
indent++; indent++;
for (Warning w : data.getWarningSet()) { for (Warning w : data.getWarningSet()) {
writeElementWithAttribute("warning", "priority", w.getPriority().getExportLabel(), TextUtil.escapeXML(w.toString())); writeln("<warning>");
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("</warning>");
} }
// Check whether to store data // Check whether to store data
@ -606,16 +623,21 @@ public class OpenRocketSaver extends RocketSaver {
// Write events // Write events
for (FlightEvent event : branch.getEvents()) { for (FlightEvent event : branch.getEvents()) {
String eventStr = "<event time=\"" + TextUtil.doubleToString(event.getTime()) String eventStr = "<event time=\"" + TextUtil.doubleToString(event.getTime())
+ "\" type=\"" + enumToXMLName(event.getType()); + "\" type=\"" + enumToXMLName(event.getType()) + "\"";
if (event.getSource() != null) { if (event.getSource() != null) {
eventStr += "\" source=\"" + TextUtil.escapeXML(event.getSource().getID()); eventStr += " source=\"" + TextUtil.escapeXML(event.getSource().getID()) + "\"";
}
if (event.getType() == FlightEvent.Type.SIM_WARN) {
eventStr += " id=\"" + TextUtil.escapeXML(((Warning) event.getData()).getID()) + "\"";
} }
if (event.getType() == FlightEvent.Type.SIM_ABORT) { if (event.getType() == FlightEvent.Type.SIM_ABORT) {
eventStr += "\" cause=\"" + enumToXMLName(((SimulationAbort)(event.getData())).getCause()); eventStr += " cause=\"" + enumToXMLName(((SimulationAbort)(event.getData())).getCause()) + "\"";
} }
eventStr += "\"/>"; eventStr += "/>";
writeln(eventStr); writeln(eventStr);
} }
@ -677,14 +699,6 @@ public class OpenRocketSaver extends RocketSaver {
writeln("<" + element + ">" + TextUtil.escapeXML(content) + "</" + element + ">"); 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 { private void writeln(String str) throws IOException {
if (str.length() == 0) { if (str.length() == 0) {
dest.write("\n"); dest.write("\n");

View File

@ -3,6 +3,7 @@ package info.openrocket.core.file.openrocket.importt;
import java.util.HashMap; import java.util.HashMap;
import java.util.UUID; import java.util.UUID;
import info.openrocket.core.logging.Message;
import info.openrocket.core.logging.SimulationAbort; import info.openrocket.core.logging.SimulationAbort;
import info.openrocket.core.logging.SimulationAbort.Cause; import info.openrocket.core.logging.SimulationAbort.Cause;
import info.openrocket.core.logging.WarningSet; import info.openrocket.core.logging.WarningSet;
@ -132,15 +133,14 @@ class FlightDataBranchHandler extends AbstractElementHandler {
if (element.equals("event")) { if (element.equals("event")) {
double time; double time;
FlightEvent.Type type; FlightEvent.Type type;
SimulationAbort abort = null; Message data = null;
SimulationAbort.Cause cause = null;
RocketComponent source = null; RocketComponent source = null;
String sourceID; String sourceID;
try { try {
time = DocumentConfig.stringToDouble(attributes.get("time")); time = DocumentConfig.stringToDouble(attributes.get("time"));
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
warnings.add("Illegal event specification, ignoring."); warnings.add("Illegal event time specification, ignoring: " + e.getMessage());
return; return;
} }
@ -157,13 +157,23 @@ class FlightDataBranchHandler extends AbstractElementHandler {
source = rocket.findComponent(UUID.fromString(sourceID)); source = rocket.findComponent(UUID.fromString(sourceID));
} }
// For aborts, get the cause // For warning events, get the warning
cause = (Cause) DocumentConfig.findEnum(attributes.get("cause"), SimulationAbort.Cause.class); if (type == FlightEvent.Type.SIM_WARN) {
if (cause != null) { data = simHandler.getWarningSet().findById(UUID.fromString(attributes.get("id")));
abort = new SimulationAbort(cause); }
// For aborts, get the cause
Cause cause = (Cause) DocumentConfig.findEnum(attributes.get("cause"), SimulationAbort.Cause.class);
if (cause != null) {
data = new SimulationAbort(cause);
}
try {
branch.addEvent(new FlightEvent(type, time, source, data));
} catch (Exception e) {
warnings.add("Illegal parameters for FlightEvent: " + e.getMessage());
} }
branch.addEvent(new FlightEvent(type, time, source, abort));
return; return;
} }

View File

@ -39,7 +39,7 @@ class FlightDataHandler extends AbstractElementHandler {
WarningSet warnings) { WarningSet warnings) {
if (element.equals("warning")) { if (element.equals("warning")) {
return PlainTextHandler.INSTANCE; return new WarningHandler(context.getOpenRocketDocument().getRocket(), warningSet);
} }
if (element.equals("databranch")) { if (element.equals("databranch")) {
if (attributes.get("name") == null || attributes.get("types") == null) { if (attributes.get("name") == null || attributes.get("types") == null) {
@ -83,10 +83,10 @@ class FlightDataHandler extends AbstractElementHandler {
if (branch.getLength() > 0) { if (branch.getLength() > 0) {
branches.add(branch); branches.add(branch);
} }
} else if (element.equals("warning")) { // } else if (element.equals("warning")) {
String priorityStr = attributes.get("priority"); // String priorityStr = attributes.get("priority");
MessagePriority priority = MessagePriority.fromExportLabel(priorityStr); // MessagePriority priority = MessagePriority.fromExportLabel(priorityStr);
warningSet.add(Warning.fromString(content, priority)); // warningSet.add(Warning.fromString(content, priority));
} }
} }
@ -159,4 +159,7 @@ class FlightDataHandler extends AbstractElementHandler {
} }
public WarningSet getWarningSet() {
return warningSet;
}
} }

View File

@ -156,6 +156,13 @@ class SingleSimulationHandler extends AbstractElementHandler {
doc.addSimulation(simulation); doc.addSimulation(simulation);
} }
/**
* @return the warning set associated with this simulation
*/
public WarningSet getWarningSet() {
return dataHandler.getWarningSet();
}
private SimulationExtension compatibilityExtension(String className) { private SimulationExtension compatibilityExtension(String className) {
JavaCode extension = Application.getInjector().getInstance(JavaCode.class); JavaCode extension = Application.getInjector().getInstance(JavaCode.class);
extension.setClassName(className); extension.setClassName(className);

View File

@ -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<RocketComponent> 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<String, String> attributes,
WarningSet warnings) {
return PlainTextHandler.INSTANCE;
}
@Override
public void closeElement(String element, HashMap<String, String> 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<String, String> 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);
}
}

View File

@ -1,17 +1,30 @@
package info.openrocket.core.logging; package info.openrocket.core.logging;
import info.openrocket.core.rocketcomponent.RocketComponent;
import java.util.Arrays; import java.util.Arrays;
import java.util.UUID;
import info.openrocket.core.rocketcomponent.RocketComponent;
/** /**
* Baseclass for logging messages (warnings, errors...) * Baseclass for logging messages (warnings, errors...)
*/ */
public abstract class Message implements Cloneable { public abstract class Message implements Cloneable {
/** Message ID **/
UUID id;
/** The rocket component(s) that are the source of this message **/ /** The rocket component(s) that are the source of this message **/
private RocketComponent[] sources = null; private RocketComponent[] sources = null;
private MessagePriority priority = MessagePriority.NORMAL; 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. * Returns the message text + message source objects.
* @return 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); 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.
* @return the rocket component(s) that are the source of this warning. Returns null if no sources are specified. * @return the rocket component(s) that are the source of this warning. Returns null if no sources are specified.

View File

@ -1,5 +1,10 @@
package info.openrocket.core.logging; 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.rocketcomponent.RocketComponent;
import info.openrocket.core.util.ArrayList; import info.openrocket.core.util.ArrayList;
import info.openrocket.core.util.BugException; 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.Monitorable;
import info.openrocket.core.util.Mutable; import info.openrocket.core.util.Mutable;
import java.util.AbstractSet;
import java.util.Iterator;
import java.util.List;
/** /**
* A set that contains multiple <code>Message</code>s. When adding a * A set that contains multiple <code>Message</code>s. When adding a
* {@link Message} to this set, the contents is checked for a message of the * {@link Message} to this set, the contents is checked for a message of the
@ -149,6 +150,14 @@ public abstract class MessageSet<E extends Message> extends AbstractSet<E> imple
return list; 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() { public void immute() {
mutable.immute(); mutable.immute();
} }

View File

@ -18,7 +18,8 @@ public abstract class Warning extends Message {
* @return a Message with the specific text and priority. * @return a Message with the specific text and priority.
*/ */
public static Warning fromString(String text, MessagePriority 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;
} }
/** /**
@ -28,8 +29,6 @@ public abstract class Warning extends Message {
return fromString(text, MessagePriority.NORMAL); return fromString(text, MessagePriority.NORMAL);
} }
///////////// Specific warning classes ///////////// ///////////// Specific warning classes /////////////

View File

@ -108,7 +108,7 @@ public class BasicEventSimulationEngine implements SimulationEngine {
// No recovery device // No recovery device
if (!simulationConfig.hasRecoveryDevice()) { 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())); currentStatus.getEventQueue().add(new FlightEvent(FlightEvent.Type.LAUNCH, 0, simulationConditions.getRocket()));
@ -315,7 +315,7 @@ public class BasicEventSimulationEngine implements SimulationEngine {
if (currentStatus.isLanded() && if (currentStatus.isLanded() &&
(event.getType() != FlightEvent.Type.ALTITUDE) && (event.getType() != FlightEvent.Type.ALTITUDE) &&
(event.getType() != FlightEvent.Type.SIMULATION_END)) (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 // Check for motor ignition events, add ignition events to queue
for (MotorClusterState state : currentStatus.getActiveMotors() ){ for (MotorClusterState state : currentStatus.getActiveMotors() ){
@ -500,12 +500,12 @@ public class BasicEventSimulationEngine implements SimulationEngine {
} }
} }
if (numActiveBelow != 1) { 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 I haven't cleared the rail yet, flag a warning
if (!currentStatus.isLaunchRodCleared()) { if (!currentStatus.isLaunchRodCleared()) {
currentStatus.getWarnings().add(Warning.EARLY_SEPARATION); currentStatus.addWarning(Warning.EARLY_SEPARATION);
} }
// Create a new simulation branch for the booster // Create a new simulation branch for the booster
@ -565,12 +565,12 @@ public class BasicEventSimulationEngine implements SimulationEngine {
// Check for launch rod // Check for launch rod
if (!currentStatus.isLaunchRodCleared()) { if (!currentStatus.isLaunchRodCleared()) {
currentStatus.getWarnings().add(Warning.RECOVERY_LAUNCH_ROD); currentStatus.addWarning(Warning.RECOVERY_LAUNCH_ROD);
} }
// Check current velocity // Check current velocity
if (currentStatus.getRocketVelocity().length() > 20) { 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); currentStatus.setLiftoff(true);

View File

@ -2,6 +2,7 @@ package info.openrocket.core.simulation;
import info.openrocket.core.l10n.Translator; import info.openrocket.core.l10n.Translator;
import info.openrocket.core.logging.SimulationAbort; import info.openrocket.core.logging.SimulationAbort;
import info.openrocket.core.logging.Warning;
import info.openrocket.core.rocketcomponent.AxialStage; import info.openrocket.core.rocketcomponent.AxialStage;
import info.openrocket.core.rocketcomponent.MotorMount; import info.openrocket.core.rocketcomponent.MotorMount;
import info.openrocket.core.rocketcomponent.RocketComponent; import info.openrocket.core.rocketcomponent.RocketComponent;
@ -81,6 +82,11 @@ public class FlightEvent implements Comparable<FlightEvent> {
*/ */
TUMBLE(trans.get("FlightEvent.Type.TUMBLE")), 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 * It is impossible for the simulation proceed due to characteristics
* of the rocket or flight configuration * of the rocket or flight configuration
@ -118,8 +124,8 @@ public class FlightEvent implements Comparable<FlightEvent> {
this(type, time, source, null); this(type, time, source, null);
} }
public FlightEvent(final FlightEvent _sourceEvent, final RocketComponent _comp, final Object _data) { public FlightEvent( final FlightEvent sourceEvent, final RocketComponent source, final Object data) {
this(_sourceEvent.type, _sourceEvent.time, _comp, _data); this(sourceEvent.type, sourceEvent.time, source, data);
} }
public FlightEvent( final Type type, final double time, final RocketComponent source, final Object data) { public FlightEvent( final Type type, final double time, final RocketComponent source, final Object data) {
@ -178,6 +184,13 @@ public class FlightEvent implements Comparable<FlightEvent> {
return this.type.ordinal() - o.type.ordinal(); 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 @Override
public String toString() { public String toString() {
return "FlightEvent[type=" + type.name() + ",time=" + time + ",source=" + source + ",data=" + String.valueOf(data) + "]"; return "FlightEvent[type=" + type.name() + ",time=" + time + ",source=" + source + ",data=" + String.valueOf(data) + "]";
@ -241,6 +254,17 @@ public class FlightEvent implements Comparable<FlightEvent> {
} }
} }
break; 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: case SIM_ABORT:
if (( null == this.data ) || ( ! ( this.data instanceof SimulationAbort ))) { if (( null == this.data ) || ( ! ( this.data instanceof SimulationAbort ))) {
throw new IllegalStateException(type.name()+" events require SimulationAbort objects"); throw new IllegalStateException(type.name()+" events require SimulationAbort objects");

View File

@ -401,7 +401,7 @@ public class RK4SimulationStepper extends AbstractSimulationStepper {
* launch rod or 0.25 seconds after departure, and when the velocity has dropped * launch rod or 0.25 seconds after departure, and when the velocity has dropped
* below 20% of the max. velocity. * below 20% of the max. velocity.
*/ */
WarningSet warnings = status.getWarnings(); WarningSet warnings = new WarningSet();
store.maxZvelocity = MathUtil.max(store.maxZvelocity, status.getRocketVelocity().z); store.maxZvelocity = MathUtil.max(store.maxZvelocity, status.getRocketVelocity().z);
if (!status.isLaunchRodCleared()) { if (!status.isLaunchRodCleared()) {
@ -421,7 +421,9 @@ public class RK4SimulationStepper extends AbstractSimulationStepper {
// Calculate aerodynamic forces // Calculate aerodynamic forces
store.forces = status.getSimulationConditions().getAerodynamicCalculator() store.forces = status.getSimulationConditions().getAerodynamicCalculator()
.getAerodynamicForces(status.getConfiguration(), store.flightConditions, warnings); .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 // Add very small randomization to yaw & pitch moments to prevent over-perfect flight
// TODO: HIGH: This should rather be performed as a listener // TODO: HIGH: This should rather be performed as a listener

View File

@ -9,6 +9,7 @@ import java.util.Set;
import info.openrocket.core.aerodynamics.FlightConditions; import info.openrocket.core.aerodynamics.FlightConditions;
import info.openrocket.core.logging.SimulationAbort; import info.openrocket.core.logging.SimulationAbort;
import info.openrocket.core.logging.Warning;
import info.openrocket.core.logging.WarningSet; import info.openrocket.core.logging.WarningSet;
import info.openrocket.core.motor.MotorConfiguration; import info.openrocket.core.motor.MotorConfiguration;
import info.openrocket.core.motor.MotorConfigurationId; import info.openrocket.core.motor.MotorConfigurationId;
@ -418,6 +419,23 @@ public class SimulationStatus implements Cloneable, Monitorable {
this.warnings = warnings; 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() { public WarningSet getWarnings() {
return warnings; return warnings;
} }

View File

@ -637,7 +637,7 @@ public class SimulationListenerHelper {
private static void warn(SimulationStatus status, SimulationListener listener) { private static void warn(SimulationStatus status, SimulationListener listener) {
if (!listener.isSystemListener()) { if (!listener.isSystemListener()) {
log.info("Non-system listener " + listener + " affected the simulation"); log.info("Non-system listener " + listener + " affected the simulation");
status.getWarnings().add(Warning.LISTENERS_AFFECTED); status.addWarning(Warning.LISTENERS_AFFECTED);
} }
} }
} }

View File

@ -2141,6 +2141,7 @@ FlightEvent.Type.STAGE_SEPARATION = Stage separation
FlightEvent.Type.APOGEE = Apogee FlightEvent.Type.APOGEE = Apogee
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = Recovery device deployment FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = Recovery device deployment
FlightEvent.Type.GROUND_HIT = Ground hit FlightEvent.Type.GROUND_HIT = Ground hit
FlightEvent.Type.SIM_WARN = Warning
FlightEvent.Type.SIM_ABORT = Simulation abort FlightEvent.Type.SIM_ABORT = Simulation abort
FlightEvent.Type.SIMULATION_END = Simulation end FlightEvent.Type.SIMULATION_END = Simulation end
FlightEvent.Type.ALTITUDE = Altitude change FlightEvent.Type.ALTITUDE = Altitude change

View File

@ -1711,6 +1711,7 @@ FlightEvent.Type.STAGE_SEPARATION = فصل المرحلة
FlightEvent.Type.APOGEE = الأوج FlightEvent.Type.APOGEE = الأوج
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = نشر جهاز الإسترداد FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = نشر جهاز الإسترداد
FlightEvent.Type.GROUND_HIT = الإصطدام بالأرض FlightEvent.Type.GROUND_HIT = الإصطدام بالأرض
FlightEvent.Type.SIM_WARN = تحذير
FlightEvent.Type.SIMULATION_END = نهاية المحاكاة FlightEvent.Type.SIMULATION_END = نهاية المحاكاة
FlightEvent.Type.ALTITUDE = تغيير الإرتفاع FlightEvent.Type.ALTITUDE = تغيير الإرتفاع
FlightEvent.Type.TUMBLE = هبوط FlightEvent.Type.TUMBLE = هبوط

View File

@ -1265,6 +1265,7 @@ FlightEvent.Type.STAGE_SEPARATION = Stufentrennung
FlightEvent.Type.APOGEE = Apogäum FlightEvent.Type.APOGEE = Apogäum
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = Auslösung des Bergungssystems FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = Auslösung des Bergungssystems
FlightEvent.Type.GROUND_HIT = Landung FlightEvent.Type.GROUND_HIT = Landung
FlightEvent.Type.SIM_WARN = Warnung
FlightEvent.Type.SIMULATION_END = Ende der Simulation FlightEvent.Type.SIMULATION_END = Ende der Simulation
FlightEvent.Type.ALTITUDE = Höhenänderung FlightEvent.Type.ALTITUDE = Höhenänderung

View File

@ -350,6 +350,7 @@ FlightEvent.Type.LAUNCH = Lanzamiento
FlightEvent.Type.LAUNCHROD = Abandono de la Gu\u00eda de lanzamiento FlightEvent.Type.LAUNCHROD = Abandono de la Gu\u00eda de lanzamiento
FlightEvent.Type.LIFTOFF = Despegue FlightEvent.Type.LIFTOFF = Despegue
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = Despliegue del sistema de recuperaci\u00f3n 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.SIMULATION_END = Fin de la simulaci\u00f3n
FlightEvent.Type.STAGE_SEPARATION = Separaci\u00f3n de etapa FlightEvent.Type.STAGE_SEPARATION = Separaci\u00f3n de etapa
FlightEvent.Type.TUMBLE = Volteo FlightEvent.Type.TUMBLE = Volteo

View File

@ -341,6 +341,7 @@ FlightEvent.Type.LAUNCH = Lancement
FlightEvent.Type.LAUNCHROD = D\u00E9gagement de rampe de lancement FlightEvent.Type.LAUNCHROD = D\u00E9gagement de rampe de lancement
FlightEvent.Type.LIFTOFF = D\u00E9collage FlightEvent.Type.LIFTOFF = D\u00E9collage
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = D\u00E9ploiement du dispositif de r\u00E9cup\u00E9ration 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.SIMULATION_END = Fin de la simulation
FlightEvent.Type.STAGE_SEPARATION = S\u00E9paration d'\u00E9tage FlightEvent.Type.STAGE_SEPARATION = S\u00E9paration d'\u00E9tage
FlightEvent.Type.TUMBLE = D\u00E9gringolade FlightEvent.Type.TUMBLE = D\u00E9gringolade

View File

@ -1269,6 +1269,7 @@ FlightEvent.Type.STAGE_SEPARATION = Separazione degli stadi
FlightEvent.Type.APOGEE = Apogeo FlightEvent.Type.APOGEE = Apogeo
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = distacco del dispositivo di recupero FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = distacco del dispositivo di recupero
FlightEvent.Type.GROUND_HIT = Atterraggio FlightEvent.Type.GROUND_HIT = Atterraggio
FlightEvent.Type,SIM_WARN = Avvisi
FlightEvent.Type.SIMULATION_END = Fine simulazione FlightEvent.Type.SIMULATION_END = Fine simulazione
FlightEvent.Type.ALTITUDE = Cambio altitudine FlightEvent.Type.ALTITUDE = Cambio altitudine

View File

@ -1316,6 +1316,7 @@ FlightEvent.Type.STAGE_SEPARATION = \u30B9\u30C6\u30FC\u30B8\u5206\u96E2
FlightEvent.Type.APOGEE = \u6700\u9AD8\u5230\u9054\u70B9 FlightEvent.Type.APOGEE = \u6700\u9AD8\u5230\u9054\u70B9
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = \u30EA\u30AB\u30D0\u30EA\u30FC\u88C5\u7F6E\u5C55\u958B FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = \u30EA\u30AB\u30D0\u30EA\u30FC\u88C5\u7F6E\u5C55\u958B
FlightEvent.Type.GROUND_HIT = \u7740\u5730 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.SIMULATION_END = \u30B7\u30DF\u30E5\u30EC\u30FC\u30B7\u30E7\u30F3\u7D42\u4E86
FlightEvent.Type.ALTITUDE = \u59FF\u52E2\u5909\u66F4 FlightEvent.Type.ALTITUDE = \u59FF\u52E2\u5909\u66F4

View File

@ -1634,6 +1634,7 @@ FlightEvent.Type.STAGE_SEPARATION = Trap afscheiding
FlightEvent.Type.APOGEE = Apogee FlightEvent.Type.APOGEE = Apogee
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = Terugvordering toestel uitrol FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = Terugvordering toestel uitrol
FlightEvent.Type.GROUND_HIT = Grond geraakt FlightEvent.Type.GROUND_HIT = Grond geraakt
FlightEvent.Type.SIM_WARN = Waarschuwing
FlightEvent.Type.SIMULATION_END = Simulatie-einde FlightEvent.Type.SIMULATION_END = Simulatie-einde
FlightEvent.Type.ALTITUDE = Hoogteverandering FlightEvent.Type.ALTITUDE = Hoogteverandering
FlightEvent.Type.TUMBLE = Tuimelen FlightEvent.Type.TUMBLE = Tuimelen

View File

@ -1210,6 +1210,7 @@ ComponentInfo.EngineBlock = <b>Blokada silnika</b> unieruchamia silnik wewn\u01
FlightEvent.Type.APOGEE = Apogeum FlightEvent.Type.APOGEE = Apogeum
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = Aktywacja uk\u0142adu odzysku FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = Aktywacja uk\u0142adu odzysku
FlightEvent.Type.GROUND_HIT = Uderzenie w ziemi\u0119 FlightEvent.Type.GROUND_HIT = Uderzenie w ziemi\u0119
FlightEvent.Type.SIM_WARN = Ostrze\u017Cenie
FlightEvent.Type.SIMULATION_END = Koniec symulacji FlightEvent.Type.SIMULATION_END = Koniec symulacji
FlightEvent.Type.ALTITUDE = Zmiana wysoko\u015Bci FlightEvent.Type.ALTITUDE = Zmiana wysoko\u015Bci

View File

@ -330,6 +330,7 @@ FlightEvent.Type.LAUNCH = Lan\u00e7amento
FlightEvent.Type.LAUNCHROD = Folga da haste de lan\u00e7amento FlightEvent.Type.LAUNCHROD = Folga da haste de lan\u00e7amento
FlightEvent.Type.LIFTOFF = Decolagem FlightEvent.Type.LIFTOFF = Decolagem
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = Implanta\u00e7\u00e3o de dispositivos de recupera\u00e7\u00e3o 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.SIMULATION_END = Final de simula\u00e7\u00e3o
FlightEvent.Type.STAGE_SEPARATION = Separa\u00e7\u00e3o do est\u00e1gio FlightEvent.Type.STAGE_SEPARATION = Separa\u00e7\u00e3o do est\u00e1gio
FlightEvent.Type.TUMBLE = Tumbling FlightEvent.Type.TUMBLE = Tumbling

View File

@ -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.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.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.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.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.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 FlightEvent.Type.TUMBLE = \u041E\u043F\u0440\u043E\u043A\u0438\u0434\u044B\u0432\u0430\u043D\u0438\u0435

View File

@ -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.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.TUMBLE = \u041f\u0435\u0440\u0435\u043a\u0438\u0434\u0430\u043d\u043d\u044f
FlightEvent.Type.EXCEPTION = \u0412\u0438\u043d\u044f\u0442\u043e\u043a FlightEvent.Type.EXCEPTION = \u0412\u0438\u043d\u044f\u0442\u043e\u043a
>>>>>>> unstable
! ThrustCurveMotorColumns ! ThrustCurveMotorColumns
TCurveMotorCol.MANUFACTURER = \u0412\u0438\u0440\u043e\u0431\u043d\u0438\u043a TCurveMotorCol.MANUFACTURER = \u0412\u0438\u0440\u043e\u0431\u043d\u0438\u043a

View File

@ -367,6 +367,7 @@ FlightEvent.Type.LAUNCH = \u53D1\u5C04
FlightEvent.Type.LAUNCHROD = \u79BB\u67B6 FlightEvent.Type.LAUNCHROD = \u79BB\u67B6
FlightEvent.Type.LIFTOFF = \u8D77\u98DE FlightEvent.Type.LIFTOFF = \u8D77\u98DE
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = \u56DE\u6536\u88C5\u7F6E\u542F\u52A8 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.SIMULATION_END = \u4EFF\u771F\u7ED3\u675F
FlightEvent.Type.STAGE_SEPARATION = \u7EA7\u95F4\u5206\u79BB FlightEvent.Type.STAGE_SEPARATION = \u7EA7\u95F4\u5206\u79BB
FlightEvent.Type.TUMBLE = \u7FFB\u6EDA FlightEvent.Type.TUMBLE = \u7FFB\u6EDA

View File

@ -1,6 +1,12 @@
package info.openrocket.core.simulation; 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.document.Simulation;
import info.openrocket.core.logging.Warning;
import info.openrocket.core.rocketcomponent.AxialStage; import info.openrocket.core.rocketcomponent.AxialStage;
import info.openrocket.core.rocketcomponent.BodyTube; import info.openrocket.core.rocketcomponent.BodyTube;
import info.openrocket.core.rocketcomponent.FlightConfigurationId; 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.simulation.exception.SimulationException;
import info.openrocket.core.util.BaseTestCase; import info.openrocket.core.util.BaseTestCase;
import info.openrocket.core.util.TestRockets; 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. * Tests to verify that simulations contain all the expected flight events.
*/ */
public class FlightEventsTest extends BaseTestCase { public class FlightEventsTest extends BaseTestCase {
private static final double EPSILON = 0.005; 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.LAUNCHROD, 0.13, null),
new FlightEvent(FlightEvent.Type.BURNOUT, 2.0, motorMountTube), new FlightEvent(FlightEvent.Type.BURNOUT, 2.0, motorMountTube),
new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 2.0, stage), 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.RECOVERY_DEVICE_DEPLOYMENT, 2.001, parachute),
new FlightEvent(FlightEvent.Type.APOGEE, 2.48, rocket), new FlightEvent(FlightEvent.Type.APOGEE, 2.48, rocket),
new FlightEvent(FlightEvent.Type.GROUND_HIT, 42.97, null), 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 ParallelStage sideBoosters = (ParallelStage) centerBoosterBody.getChild(1);
final BodyTube sideBoosterBodies = (BodyTube) sideBoosters.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 // events whose time is too variable to check are given a time of 1200
for (int b = 0; b < 2; b++) { for (int b = 0; b < 2; b++) {
FlightEvent[] expectedEvents = switch (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.BURNOUT, 2.01, centerBoosterBody),
new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 2.01, centerBooster), new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 2.01, centerBooster),
new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 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.TUMBLE, 2.85, null),
new FlightEvent(FlightEvent.Type.APOGEE, 3.78, rocket), new FlightEvent(FlightEvent.Type.APOGEE, 3.78, rocket),
new FlightEvent(FlightEvent.Type.GROUND_HIT, 9.0, null), 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]); 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 // Test that all expected events are present, in the right order, at the right
// time, from the right sources // 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 expected = expectedEvents[i];
final FlightEvent actual = actualEvents[i]; final FlightEvent actual = actualEvents[i];
assertSame(expected.getType(), actual.getType(), assertSame(expected.getType(), actual.getType(),
@ -185,6 +190,16 @@ public class FlightEventsTest extends BaseTestCase {
// Test that the event sources are correct // Test that the event sources are correct
assertEquals(expected.getSource(), actual.getSource(), assertEquals(expected.getSource(), actual.getSource(),
"Branch " + branchNo + " FlightEvent " + i + " type " + expected.getType() + " has wrong source "); "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 ");
}
} }

View File

@ -71,3 +71,5 @@ The following file format versions exist:
Added a priority attribute to simulation warnings. Added a priority attribute to simulation warnings.
Added document preferences (<docprefs>). Added document preferences (<docprefs>).
Added wind model settings (<wind mode="{average or multilevel}">), and windmodeltype to simulation conditions. Added wind model settings (<wind mode="{average or multilevel}">), and windmodeltype to simulation conditions.
Added warning flight events

View File

@ -26,7 +26,7 @@ public class CAPlot extends Plot<CADataType, CADataBranch, CAPlotConfiguration>
// Create the series for each component // Create the series for each component
List<XYSeries> allSeries = new ArrayList<>(); List<XYSeries> allSeries = new ArrayList<>();
for (int i = 0; i < components.size(); i++) { 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)); components.get(i), componentNames.get(i));
allSeries.add(series); allSeries.add(series);
} }
@ -35,10 +35,10 @@ public class CAPlot extends Plot<CADataType, CADataBranch, CAPlotConfiguration>
} }
private XYSeries createSingleSeries(int key, CADataType type, Unit unit, 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) { RocketComponent component, String componentName) {
// Default implementation for regular DataBranch // 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); branchName, baseName);
// Create a new description that includes the component name // Create a new description that includes the component name

View File

@ -9,20 +9,27 @@ import java.util.Map;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import info.openrocket.core.logging.MessagePriority;
import info.openrocket.core.logging.Warning;
import info.openrocket.core.simulation.FlightEvent; import info.openrocket.core.simulation.FlightEvent;
public class EventGraphics { 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); Color c = EVENT_COLORS.get(type);
if (c != null) if (c != null)
return c; return c;
return DEFAULT_EVENT_COLOR; return DEFAULT_EVENT_COLOR;
} }
static Image getEventImage(FlightEvent.Type type ) { static Image getEventImage(FlightEvent event) {
Image i = EVENT_IMAGES.get(type); FlightEvent.Type type = event.getType();
return i; 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); 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.SIMULATION_END, new Color(128, 0, 0));
EVENT_COLORS.put(FlightEvent.Type.TUMBLE, new Color(196, 0, 255)); 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.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)); EVENT_COLORS.put(FlightEvent.Type.SIM_ABORT, new Color(255, 0, 0));
} }
private static final Map<FlightEvent.Type, Image> EVENT_IMAGES = new HashMap<>(); private static final Map<FlightEvent.Type, Image> EVENT_IMAGES = new HashMap<>();
static { static {
loadImage(FlightEvent.Type.LAUNCH, "pix/eventicons/event-launch.png"); loadImage(EVENT_IMAGES, FlightEvent.Type.LAUNCH, "pix/eventicons/event-launch.png");
loadImage(FlightEvent.Type.LIFTOFF, "pix/eventicons/event-liftoff.png"); loadImage(EVENT_IMAGES, FlightEvent.Type.LIFTOFF, "pix/eventicons/event-liftoff.png");
loadImage(FlightEvent.Type.LAUNCHROD, "pix/eventicons/event-launchrod.png"); loadImage(EVENT_IMAGES, FlightEvent.Type.LAUNCHROD, "pix/eventicons/event-launchrod.png");
loadImage(FlightEvent.Type.IGNITION, "pix/eventicons/event-ignition.png"); loadImage(EVENT_IMAGES, FlightEvent.Type.IGNITION, "pix/eventicons/event-ignition.png");
loadImage(FlightEvent.Type.BURNOUT, "pix/eventicons/event-burnout.png"); loadImage(EVENT_IMAGES, FlightEvent.Type.BURNOUT, "pix/eventicons/event-burnout.png");
loadImage(FlightEvent.Type.EJECTION_CHARGE, "pix/eventicons/event-ejection-charge.png"); loadImage(EVENT_IMAGES, FlightEvent.Type.EJECTION_CHARGE, "pix/eventicons/event-ejection-charge.png");
loadImage(FlightEvent.Type.STAGE_SEPARATION, loadImage(EVENT_IMAGES, FlightEvent.Type.STAGE_SEPARATION,
"pix/eventicons/event-stage-separation.png"); "pix/eventicons/event-stage-separation.png");
loadImage(FlightEvent.Type.APOGEE, "pix/eventicons/event-apogee.png"); loadImage(EVENT_IMAGES, FlightEvent.Type.APOGEE, "pix/eventicons/event-apogee.png");
loadImage(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, loadImage(EVENT_IMAGES, FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT,
"pix/eventicons/event-recovery-device-deployment.png"); "pix/eventicons/event-recovery-device-deployment.png");
loadImage(FlightEvent.Type.GROUND_HIT, "pix/eventicons/event-ground-hit.png"); loadImage(EVENT_IMAGES, FlightEvent.Type.GROUND_HIT, "pix/eventicons/event-ground-hit.png");
loadImage(FlightEvent.Type.SIMULATION_END, "pix/eventicons/event-simulation-end.png"); loadImage(EVENT_IMAGES, FlightEvent.Type.SIMULATION_END, "pix/eventicons/event-simulation-end.png");
loadImage(FlightEvent.Type.EXCEPTION, "pix/eventicons/event-exception.png"); loadImage(EVENT_IMAGES, FlightEvent.Type.EXCEPTION, "pix/eventicons/event-exception.png");
loadImage(FlightEvent.Type.SIM_ABORT, "pix/eventicons/event-exception.png"); loadImage(EVENT_IMAGES, FlightEvent.Type.SIM_ABORT, "pix/eventicons/event-exception.png");
} }
private static void loadImage(FlightEvent.Type type, String file) { // Messages can happen at several priority levels, requiring different icons
private static final Map<MessagePriority, Image> MESSAGE_IMAGES = new HashMap<MessagePriority, Image>();
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 <KeyType> void loadImage(Map<KeyType, Image> imageMap, KeyType type, String file) {
InputStream is; InputStream is;
is = ClassLoader.getSystemResourceAsStream(file); is = ClassLoader.getSystemResourceAsStream(file);
@ -74,10 +90,9 @@ public class EventGraphics {
try { try {
Image image = ImageIO.read(is); Image image = ImageIO.read(is);
EVENT_IMAGES.put(type, image); imageMap.put(type, image);
} catch (IOException ignore) { } catch (IOException ignore) {
ignore.printStackTrace(); ignore.printStackTrace();
} }
} }
} }

View File

@ -1,8 +1,11 @@
package info.openrocket.swing.gui.plot; package info.openrocket.swing.gui.plot;
import info.openrocket.core.l10n.Translator; import info.openrocket.core.l10n.Translator;
import info.openrocket.core.logging.Warning;
import info.openrocket.core.simulation.DataBranch; import info.openrocket.core.simulation.DataBranch;
import info.openrocket.core.simulation.DataType; 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.startup.Application;
import info.openrocket.core.unit.Unit; import info.openrocket.core.unit.Unit;
import info.openrocket.core.unit.UnitGroup; import info.openrocket.core.unit.UnitGroup;
@ -52,6 +55,7 @@ import java.text.DecimalFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; 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 * 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<T extends DataType, B extends DataBranch<T>, C extend
protected final List<ModifiedXYItemRenderer> renderers = new ArrayList<>(); protected final List<ModifiedXYItemRenderer> renderers = new ArrayList<>();
protected final LegendItems legendItems; protected final LegendItems legendItems;
protected final XYSeriesCollection[] data; 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; protected final JFreeChart chart;
@ -101,8 +105,8 @@ public abstract class Plot<T extends DataType, B extends DataBranch<T>, C extend
// Create the data series for both axes // Create the data series for both axes
this.data = new XYSeriesCollection[2]; this.data = new XYSeriesCollection[2];
this.data[0] = new XYSeriesCollection(); this.data[Util.PlotAxisSelection.LEFT.getValue()] = new XYSeriesCollection();
this.data[1] = new XYSeriesCollection(); this.data[Util.PlotAxisSelection.RIGHT.getValue()] = new XYSeriesCollection();
// Fill the auto-selections based on first branch selected. // Fill the auto-selections based on first branch selected.
this.filledConfig = config.fillAutoAxes(mainBranch); this.filledConfig = config.fillAutoAxes(mainBranch);
@ -120,9 +124,8 @@ public abstract class Plot<T extends DataType, B extends DataBranch<T>, C extend
int seriesCount = 0; int seriesCount = 0;
// Compute the axes based on the min and max value of all branches // Compute the axes based on the min and max value of all branches
C plotConfig = filledConfig.cloneConfiguration(); filledConfig.fitAxes(allBranches);
plotConfig.fitAxes(allBranches); List<Axis> minMaxAxes = filledConfig.getAllAxes();
List<Axis> minMaxAxes = plotConfig.getAllAxes();
// Create the XYSeries objects from the flight data and store into the collections // Create the XYSeries objects from the flight data and store into the collections
String[] axisLabel = new String[2]; String[] axisLabel = new String[2];
@ -197,21 +200,40 @@ public abstract class Plot<T extends DataType, B extends DataBranch<T>, C extend
StandardXYToolTipGenerator tooltipGenerator = new StandardXYToolTipGenerator() { StandardXYToolTipGenerator tooltipGenerator = new StandardXYToolTipGenerator() {
@Override @Override
public String generateToolTip(XYDataset dataset, int series, int item) { public String generateToolTip(XYDataset dataset, int series, int item) {
XYSeriesCollection collection = data[finalAxisno]; XYSeriesCollection collection = data[finalAxisno];
if (collection.getSeriesCount() == 0) { if (collection.getSeriesCount() == 0) {
return null; return null;
} }
MetadataXYSeries ser = (MetadataXYSeries) collection.getSeries(series); 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 // Determine the appropriate name based on the time and series
String name = getNameBasedOnIdxAndSeries(ser, item); 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<Double> 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<T extends DataType, B extends DataBranch<T>, C extend
protected List<XYSeries> createSeriesForType(int dataIndex, int startIndex, T type, Unit unit, B branch, protected List<XYSeries> createSeriesForType(int dataIndex, int startIndex, T type, Unit unit, B branch,
int branchIdx, String branchName, String baseName) { int branchIdx, String branchName, String baseName) {
// Default implementation for regular DataBranch // 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<Double> plotx = branch.get(filledConfig.getDomainAxisType()); List<Double> plotx = branch.get(filledConfig.getDomainAxisType());
List<Double> ploty = branch.get(type); List<Double> ploty = branch.get(type);
@ -297,44 +319,65 @@ public abstract class Plot<T extends DataType, B extends DataBranch<T>, C extend
return type; return type;
} }
protected String formatSampleTooltip(String dataName, double dataX, String unitX, double dataY, String unitY, protected String formatTooltip(String dataName,
int sampleIdx, boolean addYValue) { String nameT, double dataT, String unitT,
String ord_end = getOrdinalEnding(sampleIdx); String nameX, double dataX, String unitX,
String nameY, double dataY, String unitY,
Set<FlightEvent> events) {
final String strFormat = "%s: %s %s<br>";
DecimalFormat df_y = DecimalFormatter.df(dataY, 2, false);
DecimalFormat df_x = DecimalFormatter.df(dataX, 2, false); DecimalFormat df_x = DecimalFormatter.df(dataX, 2, false);
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append(String.format("<html>" + sb.append("<html>");
"<b><i>%s</i></b><br>", dataName));
if (addYValue) { sb.append(String.format("<b><i>%s</i></b><br>", dataName));
sb.append(String.format("Y: %s %s<br>", df_y.format(dataY), unitY));
// 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("<b><i>Warning: " + ((Warning) event.getData()).toString() + "</b></i><br>");
}
} }
sb.append(String.format("X: %s %s<br>" + // Now pass through and collect the other events
"%d<sup>%s</sup> sample" + String eventStr = "";
"</html>", df_x.format(dataX), unitX, sampleIdx, ord_end)); for (FlightEvent event : events) {
if (event.getType() != FlightEvent.Type.SIM_WARN) {
if (eventStr != "") {
eventStr = eventStr + ", ";
}
eventStr = eventStr + event.getType();
}
}
sb.append(eventStr + "<br>");
}
// 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("</html>");
return sb.toString(); return sb.toString();
} }
protected String formatSampleTooltip(String dataName, double dataX, String unitX, double dataY, String unitY, int sampleIdx) { protected String formatTooltip(String dataName, String nameX, double dataX, String unitX) {
return formatSampleTooltip(dataName, dataX, unitX, dataY, unitY, sampleIdx, true); return formatTooltip(dataName, "", Double.NaN, "", nameX, dataX, unitX, "", Double.NaN, "", null);
}
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 static class LegendItems implements LegendItemSource { protected static class LegendItems implements LegendItemSource {
@ -540,20 +583,27 @@ public abstract class Plot<T extends DataType, B extends DataBranch<T>, C extend
protected static class MetadataXYSeries extends XYSeries { protected static class MetadataXYSeries extends XYSeries {
private final int branchIdx; private final int branchIdx;
private final int dataIdx;
private final String unit; private final String unit;
private final String branchName; private final String branchName;
private String baseName; 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) { String branchName, String baseName) {
super(key, autoSort, allowDuplicateXValues); super(key, autoSort, allowDuplicateXValues);
this.branchIdx = branchIdx; this.branchIdx = branchIdx;
this.dataIdx = dataIdx;
this.unit = unit; this.unit = unit;
this.branchName = branchName; this.branchName = branchName;
this.baseName = baseName; this.baseName = baseName;
updateDescription(); 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() { public String getUnit() {
return unit; return unit;
} }
@ -562,6 +612,10 @@ public abstract class Plot<T extends DataType, B extends DataBranch<T>, C extend
return branchIdx; return branchIdx;
} }
public int getDataIdx() {
return dataIdx;
}
public String getBranchName() { public String getBranchName() {
return branchName; return branchName;
} }

View File

@ -5,6 +5,7 @@ import java.awt.Font;
import java.awt.Image; import java.awt.Image;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.text.DecimalFormat;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -13,11 +14,13 @@ import java.util.Set;
import info.openrocket.core.document.Simulation; import info.openrocket.core.document.Simulation;
import info.openrocket.core.logging.SimulationAbort; import info.openrocket.core.logging.SimulationAbort;
import info.openrocket.core.logging.Warning;
import info.openrocket.core.simulation.FlightDataBranch; import info.openrocket.core.simulation.FlightDataBranch;
import info.openrocket.core.simulation.FlightDataType; import info.openrocket.core.simulation.FlightDataType;
import info.openrocket.core.simulation.FlightEvent; import info.openrocket.core.simulation.FlightEvent;
import info.openrocket.core.preferences.ApplicationPreferences; import info.openrocket.core.preferences.ApplicationPreferences;
import info.openrocket.core.util.LinearInterpolator; import info.openrocket.core.util.LinearInterpolator;
import info.openrocket.swing.utils.DecimalFormatter;
import org.jfree.chart.annotations.XYImageAnnotation; import org.jfree.chart.annotations.XYImageAnnotation;
import org.jfree.chart.annotations.XYTitleAnnotation; 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.RectangleAnchor;
import org.jfree.chart.ui.RectangleEdge; import org.jfree.chart.ui.RectangleEdge;
import org.jfree.chart.ui.RectangleInsets; import org.jfree.chart.ui.RectangleInsets;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class SimulationPlot extends Plot<FlightDataType, FlightDataBranch, SimulationPlotConfiguration> { public class SimulationPlot extends Plot<FlightDataType, FlightDataBranch, SimulationPlotConfiguration> {
@ -106,32 +111,31 @@ public class SimulationPlot extends Plot<FlightDataType, FlightDataBranch, Simul
plot.clearAnnotations(); plot.clearAnnotations();
// Store flight event information // Store flight event information
List<Double> eventTimes = new ArrayList<>();
List<String> eventLabels = new ArrayList<>();
List<Color> eventColors = new ArrayList<>(); List<Color> eventColors = new ArrayList<>();
List<Image> eventImages = new ArrayList<>(); List<Image> eventImages = new ArrayList<>();
List<Set<FlightEvent>> eventSets = new ArrayList<>();
// Plot the markers // Plot the markers
if (config.getDomainAxisType() == FlightDataType.TYPE_TIME && !preferences.getBoolean(ApplicationPreferences.MARKER_STYLE_ICON, false)) { if (config.getDomainAxisType() == FlightDataType.TYPE_TIME && !preferences.getBoolean(ApplicationPreferences.MARKER_STYLE_ICON, false)) {
fillEventLists(branch, eventTimes, eventLabels, eventColors, eventImages); fillEventLists(branch, eventColors, eventImages, eventSets);
plotVerticalLineMarkers(plot, eventTimes, eventLabels, eventColors); plotVerticalLineMarkers(plot, eventColors, eventSets);
} else { // Other domains are plotted as image annotations } else { // Other domains are plotted as image annotations
if (branch == -1) { if (branch == -1) {
// For icon markers, we need to do the plotting separately, otherwise you can have icon markers from e.g. // 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 // branch 1 be plotted on branch 0. Also, need to take the branches in reverse order so lower number
for (int b = 0; b < simulation.getSimulatedData().getBranchCount(); b++) { // branches get their icons put on top of higher number and the tooltip headings are correct.
fillEventLists(b, eventTimes, eventLabels, eventColors, eventImages); for (int b = simulation.getSimulatedData().getBranchCount() - 1; b >= 0; b--) {
fillEventLists(b, eventColors, eventImages, eventSets);
dataBranch = simulation.getSimulatedData().getBranch(b); dataBranch = simulation.getSimulatedData().getBranch(b);
plotIconMarkers(plot, dataBranch, eventTimes, eventLabels, eventImages); plotIconMarkers(plot, simulation, b, eventImages, eventSets);
eventTimes.clear(); eventSets = new ArrayList<>();
eventLabels.clear();
eventColors.clear(); eventColors.clear();
eventImages.clear(); eventImages.clear();
} }
} else { } else {
fillEventLists(branch, eventTimes, eventLabels, eventColors, eventImages); fillEventLists(branch, eventColors, eventImages, eventSets);
plotIconMarkers(plot, dataBranch, eventTimes, eventLabels, eventImages); plotIconMarkers(plot, simulation, branch, eventImages, eventSets);
} }
} }
} }
@ -154,78 +158,92 @@ public class SimulationPlot extends Plot<FlightDataType, FlightDataBranch, Simul
double separationTime = allBranches.get(i).getSeparationTime(); double separationTime = allBranches.get(i).getSeparationTime();
int separationIdx = allBranches.get(i).getDataIndexOfTime(separationTime); int separationIdx = allBranches.get(i).getDataIndexOfTime(separationTime);
// If the separation time is after the current data index, the stage is still attached, so add the // If the separation time is at or after the current data index, the stage is still attached, so add the
// stage name to the series name // stage name to the series name
if (separationIdx != -1 && separationIdx > dataIdx) { if (separationIdx != -1 && separationIdx >= dataIdx) {
newBranchName.append(" + ").append(allBranches.get(i).getName()); newBranchName.append(" + ").append(allBranches.get(i).getName());
} }
} }
return newBranchName + ": " + ser.getBaseName(); return newBranchName + ": " + ser.getBaseName();
} }
private void fillEventLists(int branch, List<Double> eventTimes, List<String> eventLabels, private void fillEventLists(int branch,
List<Color> eventColors, List<Image> eventImages) { List<Color> eventColors, List<Image> eventImages, List<Set<FlightEvent>> eventSets) {
Set<FlightEvent> eventSet = new HashSet<>();
Set<FlightEvent.Type> typeSet = new HashSet<>(); Set<FlightEvent.Type> typeSet = new HashSet<>();
double prevTime = -100; double prevTime = -100;
String text = null;
Color color = null; Color color = null;
Image image = null; Image image = null;
int maxOrdinal = -1; int maxOrdinal = -1;
for (EventDisplayInfo info : eventList) { for (EventDisplayInfo info : eventList) {
if (branch >= 0 && branch != info.stage) { if (branch >= 0 && branch != info.stage) {
continue; continue;
} }
double t = info.time; 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 (Math.abs(t - prevTime) <= 0.05) {
if (!typeSet.contains(type)) { if (!typeSet.contains(type)) {
text = text + ", " + type.toString();
if (type.ordinal() > maxOrdinal) { if (type.ordinal() > maxOrdinal) {
color = EventGraphics.getEventColor(type); color = EventGraphics.getEventColor(event);
image = EventGraphics.getEventImage(type); image = EventGraphics.getEventImage(event);
maxOrdinal = type.ordinal(); maxOrdinal = type.ordinal();
} }
typeSet.add(type); typeSet.add(type);
eventSet.add(event);
} }
} else { } else {
if (text != null) { if (!eventSet.isEmpty()) {
eventTimes.add(prevTime);
eventLabels.add(text);
eventColors.add(color); eventColors.add(color);
eventImages.add(image); eventImages.add(image);
eventSets.add(eventSet);
} }
prevTime = t; prevTime = t;
text = type.toString(); color = EventGraphics.getEventColor(event);
color = EventGraphics.getEventColor(type); image = EventGraphics.getEventImage(event);
image = EventGraphics.getEventImage(type);
typeSet.clear(); typeSet.clear();
typeSet.add(type); typeSet.add(type);
eventSet = new HashSet<>();
eventSet.add(event);
maxOrdinal = type.ordinal(); maxOrdinal = type.ordinal();
} }
} }
if (text != null) { if (!eventSet.isEmpty()) {
eventTimes.add(prevTime);
eventLabels.add(text);
eventColors.add(color); eventColors.add(color);
eventImages.add(image); eventImages.add(image);
eventSets.add(eventSet);
} }
} }
private static void plotVerticalLineMarkers(XYPlot plot, List<Double> eventTimes, List<String> eventLabels, List<Color> eventColors) { private static String constructEventLabels(Set<FlightEvent> events) {
String text = "";
for (FlightEvent event : events) {
if (text != "") {
text += ", ";
}
text += event.getType().toString();
}
return text;
}
private static void plotVerticalLineMarkers(XYPlot plot, List<Color> eventColors, List<Set<FlightEvent>> eventSets) {
double markerWidth = 0.01 * plot.getDomainAxis().getUpperBound(); double markerWidth = 0.01 * plot.getDomainAxis().getUpperBound();
// Domain time is plotted as vertical lines // Domain time is plotted as vertical lines
for (int i = 0; i < eventTimes.size(); i++) { for (int i = 0; i < eventSets.size(); i++) {
double t = eventTimes.get(i); Set<FlightEvent> events = eventSets.get(i);
String event = eventLabels.get(i); double t = ((FlightEvent)events.toArray()[0]).getTime();
String eventLabel = constructEventLabels(events);
Color color = eventColors.get(i); Color color = eventColors.get(i);
ValueMarker m = new ValueMarker(t); ValueMarker m = new ValueMarker(t);
m.setLabel(event); m.setLabel(eventLabel);
m.setPaint(color); m.setPaint(color);
m.setLabelPaint(color); m.setLabelPaint(color);
m.setAlpha(0.7f); m.setAlpha(0.7f);
@ -238,40 +256,60 @@ public class SimulationPlot extends Plot<FlightDataType, FlightDataBranch, Simul
} }
} }
private void plotIconMarkers(XYPlot plot, FlightDataBranch dataBranch, List<Double> eventTimes, private void plotIconMarkers(XYPlot plot, Simulation simulation, int branch, List<Image> eventImages, List<Set<FlightEvent>> eventSets) {
List<String> eventLabels, List<Image> eventImages) {
FlightDataBranch dataBranch = simulation.getSimulatedData().getBranch(branch);
List<Double> time = dataBranch.get(FlightDataType.TYPE_TIME); List<Double> time = dataBranch.get(FlightDataType.TYPE_TIME);
String tName = FlightDataType.TYPE_TIME.getName();
List<Double> domain = dataBranch.get(config.getDomainAxisType()); List<Double> domain = dataBranch.get(config.getDomainAxisType());
LinearInterpolator domainInterpolator = new LinearInterpolator(time, domain); LinearInterpolator domainInterpolator = new LinearInterpolator(time, domain);
String xName = config.getDomainAxisType().getName();
for (int i = 0; i < eventTimes.size(); i++) { List<Axis> minMaxAxes = filledConfig.getAllAxes();
double t = eventTimes.get(i);
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<MetadataXYSeries>)(collection.getSeries())) {
if (series.getBranchIdx() != branch) {
continue;
}
int dataTypeIdx = series.getDataIdx();
FlightDataType type = config.getType(dataTypeIdx);
String yName = type.toString();
List<Double> range = dataBranch.get(type);
LinearInterpolator rangeInterpolator = new LinearInterpolator(time, range);
for (int i = 0; i < eventSets.size(); i++) {
Set<FlightEvent> events = eventSets.get(i);
double t = ((FlightEvent)events.toArray()[0]).getTime();
Image image = eventImages.get(i); Image image = eventImages.get(i);
if (image == null) { if (image == null) {
continue; continue;
} }
double xcoord = domainInterpolator.getValue(t); double xcoord = domainInterpolator.getValue(t);
for (int index = 0; index < config.getDataCount(); index++) {
FlightDataType type = config.getType(index);
List<Double> 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()) {
continue;
}
double ycoord = rangeInterpolator.getValue(t); double ycoord = rangeInterpolator.getValue(t);
if (!Double.isNaN(ycoord)) {
// Convert units
xcoord = config.getDomainAxisUnit().toUnit(xcoord);
ycoord = config.getUnit(index).toUnit(ycoord);
xcoord = config.getDomainAxisUnit().toUnit(xcoord);
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, // Get the sample index of the flight event. Because this can be an interpolation between two samples,
// take the closest sample. // take the closest sample.
final int sampleIdx; final int sampleIdx;
@ -279,16 +317,28 @@ public class SimulationPlot extends Plot<FlightDataType, FlightDataBranch, Simul
.min(Comparator.comparingDouble(sample -> Math.abs(sample - t))); .min(Comparator.comparingDouble(sample -> Math.abs(sample - t)));
sampleIdx = closestSample.map(time::indexOf).orElse(-1); sampleIdx = closestSample.map(time::indexOf).orElse(-1);
String tooltipText = formatSampleTooltip(eventLabels.get(i), xcoord, config.getDomainAxisUnit().getUnit(), sampleIdx) ; // 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 = XYImageAnnotation annotation =
new XYImageAnnotation(xcoord, ycoord, image, RectangleAnchor.CENTER); new XYImageAnnotation(xcoord, yloc, image, RectangleAnchor.CENTER);
annotation.setToolTipText(tooltipText); annotation.setToolTipText(tooltipText);
plot.addAnnotation(annotation); plot.addAnnotation(annotation);
} }
} }
} }
} }
}
}
private List<EventDisplayInfo> buildEventInfo() { private List<EventDisplayInfo> buildEventInfo() {
ArrayList<EventDisplayInfo> eventList = new ArrayList<>(); ArrayList<EventDisplayInfo> eventList = new ArrayList<>();

View File

@ -32,6 +32,7 @@ public class SimulationPlotConfiguration extends PlotConfiguration<FlightDataTyp
config.setEvent(FlightEvent.Type.GROUND_HIT, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true);
config.setEvent(FlightEvent.Type.TUMBLE, true); config.setEvent(FlightEvent.Type.TUMBLE, true);
config.setEvent(FlightEvent.Type.EXCEPTION, true); config.setEvent(FlightEvent.Type.EXCEPTION, true);
config.setEvent(FlightEvent.Type.SIM_WARN, true);
config.setEvent(FlightEvent.Type.SIM_ABORT, true); config.setEvent(FlightEvent.Type.SIM_ABORT, true);
configs.add(config); configs.add(config);
@ -48,6 +49,7 @@ public class SimulationPlotConfiguration extends PlotConfiguration<FlightDataTyp
config.setEvent(FlightEvent.Type.GROUND_HIT, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true);
config.setEvent(FlightEvent.Type.TUMBLE, true); config.setEvent(FlightEvent.Type.TUMBLE, true);
config.setEvent(FlightEvent.Type.EXCEPTION, true); config.setEvent(FlightEvent.Type.EXCEPTION, true);
config.setEvent(FlightEvent.Type.SIM_WARN, true);
config.setEvent(FlightEvent.Type.SIM_ABORT, true); config.setEvent(FlightEvent.Type.SIM_ABORT, true);
configs.add(config); configs.add(config);
@ -62,6 +64,7 @@ public class SimulationPlotConfiguration extends PlotConfiguration<FlightDataTyp
config.setEvent(FlightEvent.Type.GROUND_HIT, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true);
config.setEvent(FlightEvent.Type.TUMBLE, true); config.setEvent(FlightEvent.Type.TUMBLE, true);
config.setEvent(FlightEvent.Type.EXCEPTION, true); config.setEvent(FlightEvent.Type.EXCEPTION, true);
config.setEvent(FlightEvent.Type.SIM_WARN, true);
config.setEvent(FlightEvent.Type.SIM_ABORT, true); config.setEvent(FlightEvent.Type.SIM_ABORT, true);
configs.add(config); configs.add(config);
@ -76,6 +79,7 @@ public class SimulationPlotConfiguration extends PlotConfiguration<FlightDataTyp
config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
config.setEvent(FlightEvent.Type.GROUND_HIT, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true);
config.setEvent(FlightEvent.Type.EXCEPTION, true); config.setEvent(FlightEvent.Type.EXCEPTION, true);
config.setEvent(FlightEvent.Type.SIM_WARN, true);
config.setEvent(FlightEvent.Type.SIM_ABORT, true); config.setEvent(FlightEvent.Type.SIM_ABORT, true);
configs.add(config); configs.add(config);
@ -91,6 +95,8 @@ public class SimulationPlotConfiguration extends PlotConfiguration<FlightDataTyp
config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
config.setEvent(FlightEvent.Type.GROUND_HIT, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true);
config.setEvent(FlightEvent.Type.TUMBLE, true); config.setEvent(FlightEvent.Type.TUMBLE, true);
config.setEvent(FlightEvent.Type.EXCEPTION, true);
config.setEvent(FlightEvent.Type.SIM_WARN, true);
config.setEvent(FlightEvent.Type.SIM_ABORT, true); config.setEvent(FlightEvent.Type.SIM_ABORT, true);
configs.add(config); configs.add(config);
@ -102,6 +108,7 @@ public class SimulationPlotConfiguration extends PlotConfiguration<FlightDataTyp
config.addPlotDataType(FlightDataType.TYPE_BASE_DRAG_COEFF, 0); config.addPlotDataType(FlightDataType.TYPE_BASE_DRAG_COEFF, 0);
config.addPlotDataType(FlightDataType.TYPE_PRESSURE_DRAG_COEFF, 0); config.addPlotDataType(FlightDataType.TYPE_PRESSURE_DRAG_COEFF, 0);
config.setEvent(FlightEvent.Type.EXCEPTION, true); config.setEvent(FlightEvent.Type.EXCEPTION, true);
config.setEvent(FlightEvent.Type.SIM_WARN, true);
config.setEvent(FlightEvent.Type.SIM_ABORT, true); config.setEvent(FlightEvent.Type.SIM_ABORT, true);
configs.add(config); configs.add(config);
@ -120,6 +127,7 @@ public class SimulationPlotConfiguration extends PlotConfiguration<FlightDataTyp
config.setEvent(FlightEvent.Type.GROUND_HIT, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true);
config.setEvent(FlightEvent.Type.TUMBLE, true); config.setEvent(FlightEvent.Type.TUMBLE, true);
config.setEvent(FlightEvent.Type.EXCEPTION, true); config.setEvent(FlightEvent.Type.EXCEPTION, true);
config.setEvent(FlightEvent.Type.SIM_WARN, true);
config.setEvent(FlightEvent.Type.SIM_ABORT, true); config.setEvent(FlightEvent.Type.SIM_ABORT, true);
configs.add(config); configs.add(config);
@ -136,6 +144,7 @@ public class SimulationPlotConfiguration extends PlotConfiguration<FlightDataTyp
config.setEvent(FlightEvent.Type.GROUND_HIT, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true);
config.setEvent(FlightEvent.Type.TUMBLE, true); config.setEvent(FlightEvent.Type.TUMBLE, true);
config.setEvent(FlightEvent.Type.EXCEPTION, true); config.setEvent(FlightEvent.Type.EXCEPTION, true);
config.setEvent(FlightEvent.Type.SIM_WARN, true);
config.setEvent(FlightEvent.Type.SIM_ABORT, true); config.setEvent(FlightEvent.Type.SIM_ABORT, true);
configs.add(config); configs.add(config);
@ -151,6 +160,7 @@ public class SimulationPlotConfiguration extends PlotConfiguration<FlightDataTyp
config.setEvent(FlightEvent.Type.GROUND_HIT, true); config.setEvent(FlightEvent.Type.GROUND_HIT, true);
config.setEvent(FlightEvent.Type.TUMBLE, true); config.setEvent(FlightEvent.Type.TUMBLE, true);
config.setEvent(FlightEvent.Type.EXCEPTION, true); config.setEvent(FlightEvent.Type.EXCEPTION, true);
config.setEvent(FlightEvent.Type.SIM_WARN, true);
config.setEvent(FlightEvent.Type.SIM_ABORT, true); config.setEvent(FlightEvent.Type.SIM_ABORT, true);
configs.add(config); configs.add(config);

View File

@ -174,8 +174,7 @@ public class SimulationPlotPanel extends PlotPanel<FlightDataType, FlightDataBra
col0.setPreferredWidth(w); col0.setPreferredWidth(w);
col0.setMaxWidth(w); col0.setMaxWidth(w);
table.addMouseListener(new GUIUtil.BooleanTableClickListener(table)); table.addMouseListener(new GUIUtil.BooleanTableClickListener(table));
selectorPanel.add(new JScrollPane(table), "height 200px, width 200lp, grow 1, wrap rel"); selectorPanel.add(new JScrollPane(table), "width 200lp, grow 1, wrap rel");
//// All + None buttons //// All + None buttons
JButton button = new JButton(trans.get("simplotpanel.but.All")); JButton button = new JButton(trans.get("simplotpanel.but.All"));