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() {
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;
}
}

View File

@ -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,

View File

@ -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("<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
@ -606,16 +623,21 @@ public class OpenRocketSaver extends RocketSaver {
// Write events
for (FlightEvent event : branch.getEvents()) {
String eventStr = "<event time=\"" + TextUtil.doubleToString(event.getTime())
+ "\" type=\"" + enumToXMLName(event.getType());
+ "\" type=\"" + enumToXMLName(event.getType()) + "\"";
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) {
eventStr += "\" cause=\"" + enumToXMLName(((SimulationAbort)(event.getData())).getCause());
eventStr += " cause=\"" + enumToXMLName(((SimulationAbort)(event.getData())).getCause()) + "\"";
}
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) {

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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);

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

@ -80,4 +80,4 @@ public interface ElementHandler {
public abstract void endHandler(String element, HashMap<String, String> attributes,
String content, WarningSet warnings) throws SAXException;
}
}

View File

@ -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.

View File

@ -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 <code>Message</code>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<E extends Message> extends AbstractSet<E> 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<E extends Message> extends AbstractSet<E> imple
}
m.setSources(sources);
return add(m);
}
}
/**
* Add a <code>Message</code> of the specified type with the specified discriminator to the
@ -149,6 +150,14 @@ public abstract class MessageSet<E extends Message> extends AbstractSet<E> 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();
}

View File

@ -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 /////////////

View File

@ -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);

View File

@ -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<FlightEvent> {
*/
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<FlightEvent> {
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<FlightEvent> {
// 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<FlightEvent> {
}
}
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");

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
* 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

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -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 = هبوط

View File

@ -1265,6 +1265,7 @@ FlightEvent.Type.STAGE_SEPARATION = Stufentrennung
FlightEvent.Type.APOGEE = Apogäum
FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT = Auslösung des Bergungssystems
FlightEvent.Type.GROUND_HIT = Landung
FlightEvent.Type.SIM_WARN = Warnung
FlightEvent.Type.SIMULATION_END = Ende der Simulation
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.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

View File

@ -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

View File

@ -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

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.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

View File

@ -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

View File

@ -1210,6 +1210,7 @@ ComponentInfo.EngineBlock = <b>Blokada silnika</b> 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

View File

@ -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

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.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

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.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

View File

@ -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

View File

@ -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 ");
}
}

View File

@ -71,3 +71,5 @@ The following file format versions exist:
Added a priority attribute to simulation warnings.
Added document preferences (<docprefs>).
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
List<XYSeries> 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<CADataType, CADataBranch, CAPlotConfiguration>
}
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

View File

@ -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<FlightEvent.Type, Image> 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<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 void loadImage(FlightEvent.Type type, String file) {
private static <KeyType> void loadImage(Map<KeyType, Image> 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();
}
}
}

View File

@ -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<T extends DataType, B extends DataBranch<T>, C extend
protected final List<ModifiedXYItemRenderer> 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<T extends DataType, B extends DataBranch<T>, 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<T extends DataType, B extends DataBranch<T>, 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<Axis> minMaxAxes = plotConfig.getAllAxes();
filledConfig.fitAxes(allBranches);
List<Axis> 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<T extends DataType, B extends DataBranch<T>, 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<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,
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<Double> plotx = branch.get(filledConfig.getDomainAxisType());
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;
}
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<FlightEvent> events) {
DecimalFormat df_y = DecimalFormatter.df(dataY, 2, false);
final String strFormat = "%s: %s %s<br>";
DecimalFormat df_x = DecimalFormatter.df(dataX, 2, false);
StringBuilder sb = new StringBuilder();
sb.append(String.format("<html>" +
"<b><i>%s</i></b><br>", dataName));
sb.append("<html>");
if (addYValue) {
sb.append(String.format("Y: %s %s<br>", df_y.format(dataY), unitY));
sb.append(String.format("<b><i>%s</i></b><br>", 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("<b><i>Warning: " + ((Warning) event.getData()).toString() + "</b></i><br>");
}
}
// 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 + "<br>");
}
sb.append(String.format("X: %s %s<br>" +
"%d<sup>%s</sup> sample" +
"</html>", 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("</html>");
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<T extends DataType, B extends DataBranch<T>, 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<T extends DataType, B extends DataBranch<T>, C extend
return branchIdx;
}
public int getDataIdx() {
return dataIdx;
}
public String getBranchName() {
return branchName;
}

View File

@ -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<FlightDataType, FlightDataBranch, SimulationPlotConfiguration> {
@ -106,32 +111,31 @@ public class SimulationPlot extends Plot<FlightDataType, FlightDataBranch, Simul
plot.clearAnnotations();
// Store flight event information
List<Double> eventTimes = new ArrayList<>();
List<String> eventLabels = new ArrayList<>();
List<Color> eventColors = new ArrayList<>();
List<Image> eventImages = new ArrayList<>();
List<Set<FlightEvent>> 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<FlightDataType, FlightDataBranch, Simul
double separationTime = allBranches.get(i).getSeparationTime();
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
if (separationIdx != -1 && separationIdx > dataIdx) {
if (separationIdx != -1 && separationIdx >= dataIdx) {
newBranchName.append(" + ").append(allBranches.get(i).getName());
}
}
return newBranchName + ": " + ser.getBaseName();
}
private void fillEventLists(int branch, List<Double> eventTimes, List<String> eventLabels,
List<Color> eventColors, List<Image> eventImages) {
private void fillEventLists(int branch,
List<Color> eventColors, List<Image> eventImages, List<Set<FlightEvent>> eventSets) {
Set<FlightEvent> eventSet = new HashSet<>();
Set<FlightEvent.Type> 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<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();
// 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<FlightEvent> 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<FlightDataType, FlightDataBranch, Simul
}
}
private void plotIconMarkers(XYPlot plot, FlightDataBranch dataBranch, List<Double> eventTimes,
List<String> eventLabels, List<Image> eventImages) {
private void plotIconMarkers(XYPlot plot, Simulation simulation, int branch, List<Image> eventImages, List<Set<FlightEvent>> eventSets) {
FlightDataBranch dataBranch = simulation.getSimulatedData().getBranch(branch);
List<Double> time = dataBranch.get(FlightDataType.TYPE_TIME);
String tName = FlightDataType.TYPE_TIME.getName();
List<Double> 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<Axis> 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<MetadataXYSeries>)(collection.getSeries())) {
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()) {
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<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);
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<Double> 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<Double> 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);
}
}
}
}
}

View File

@ -32,6 +32,7 @@ public class SimulationPlotConfiguration extends PlotConfiguration<FlightDataTyp
config.setEvent(FlightEvent.Type.GROUND_HIT, 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);
configs.add(config);
@ -48,6 +49,7 @@ public class SimulationPlotConfiguration extends PlotConfiguration<FlightDataTyp
config.setEvent(FlightEvent.Type.GROUND_HIT, 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);
configs.add(config);
@ -62,6 +64,7 @@ public class SimulationPlotConfiguration extends PlotConfiguration<FlightDataTyp
config.setEvent(FlightEvent.Type.GROUND_HIT, 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);
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.GROUND_HIT, true);
config.setEvent(FlightEvent.Type.EXCEPTION, true);
config.setEvent(FlightEvent.Type.SIM_WARN, true);
config.setEvent(FlightEvent.Type.SIM_ABORT, true);
configs.add(config);
@ -91,6 +95,8 @@ public class SimulationPlotConfiguration extends PlotConfiguration<FlightDataTyp
config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
config.setEvent(FlightEvent.Type.GROUND_HIT, 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);
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_PRESSURE_DRAG_COEFF, 0);
config.setEvent(FlightEvent.Type.EXCEPTION, true);
config.setEvent(FlightEvent.Type.SIM_WARN, true);
config.setEvent(FlightEvent.Type.SIM_ABORT, true);
configs.add(config);
@ -120,6 +127,7 @@ public class SimulationPlotConfiguration extends PlotConfiguration<FlightDataTyp
config.setEvent(FlightEvent.Type.GROUND_HIT, 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);
configs.add(config);
@ -136,6 +144,7 @@ public class SimulationPlotConfiguration extends PlotConfiguration<FlightDataTyp
config.setEvent(FlightEvent.Type.GROUND_HIT, 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);
configs.add(config);
@ -151,6 +160,7 @@ public class SimulationPlotConfiguration extends PlotConfiguration<FlightDataTyp
config.setEvent(FlightEvent.Type.GROUND_HIT, 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);
configs.add(config);

View File

@ -174,8 +174,7 @@ public class SimulationPlotPanel extends PlotPanel<FlightDataType, FlightDataBra
col0.setPreferredWidth(w);
col0.setMaxWidth(w);
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
JButton button = new JButton(trans.get("simplotpanel.but.All"));