diff --git a/core/src/net/sf/openrocket/file/CSVExport.java b/core/src/net/sf/openrocket/file/CSVExport.java index b8e1cc4a1..886ebc235 100644 --- a/core/src/net/sf/openrocket/file/CSVExport.java +++ b/core/src/net/sf/openrocket/file/CSVExport.java @@ -92,9 +92,11 @@ public class CSVExport { private static void writeData(PrintWriter writer, FlightDataBranch branch, FlightDataType[] fields, Unit[] units, String fieldSeparator, int decimalPlaces, boolean isExponentialNotation, boolean eventComments, String commentStarter) { + // Time variable + List time = branch.get(FlightDataType.TYPE_TIME); // Number of data points - int n = branch.getLength(); + int n = time != null ? time.size() : branch.getLength(); // Flight events in occurrence order List events = branch.getEvents(); @@ -102,15 +104,14 @@ public class CSVExport { int eventPosition = 0; // List of field values - List> fieldValues = new ArrayList>(); + List> fieldValues = new ArrayList<>(); for (FlightDataType t : fields) { - fieldValues.add(branch.get(t)); + List values = branch.get(t); + fieldValues.add(values); } - // Time variable - List time = branch.get(FlightDataType.TYPE_TIME); + // If time information is not available, print events at beginning of file if (eventComments && time == null) { - // If time information is not available, print events at beginning of file for (FlightEvent e : events) { printEvent(writer, e, commentStarter); } diff --git a/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java b/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java index 9169348c6..048db84c4 100644 --- a/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java +++ b/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java @@ -216,21 +216,22 @@ public class OpenRocketSaver extends RocketSaver { /* * NOTE: Remember to update the supported versions in DocumentConfig as well! * - * File version 1.8 is required for: + * File version 1.9 is required for: * - new-style positioning * - external/parallel booster stages * - external pods * - Rail Buttons + * - Flight event source saving * - * Otherwise use version 1.8. + * Otherwise use version 1.9. */ ///////////////// - // Version 1.8 // + // Version 1.9 // ///////////////// // for any new-style positioning: 'axialoffset', 'angleoffset', 'radiusoffset' tags // these tags are used for any RocketComponent child classes positioning... so... ALL the classes. - return FILE_VERSION_DIVISOR + 8; + return FILE_VERSION_DIVISOR + 9; } @@ -531,8 +532,13 @@ public class OpenRocketSaver extends RocketSaver { // Write events for (FlightEvent event : branch.getEvents()) { - writeln(""); + String eventStr = ""; + writeln(eventStr); } // Write the data diff --git a/core/src/net/sf/openrocket/file/openrocket/importt/DocumentConfig.java b/core/src/net/sf/openrocket/file/openrocket/importt/DocumentConfig.java index 82428f119..6995c8210 100644 --- a/core/src/net/sf/openrocket/file/openrocket/importt/DocumentConfig.java +++ b/core/src/net/sf/openrocket/file/openrocket/importt/DocumentConfig.java @@ -52,7 +52,7 @@ import net.sf.openrocket.util.Reflection; class DocumentConfig { /* Remember to update OpenRocketSaver as well! */ - public static final String[] SUPPORTED_VERSIONS = { "1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6", "1.7", "1.8" }; + public static final String[] SUPPORTED_VERSIONS = { "1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6", "1.7", "1.8", "1.9" }; /** * Divisor used in converting an integer version to the point-represented version. @@ -113,6 +113,8 @@ class DocumentConfig { // RocketComponent setters.put("RocketComponent:name", new StringSetter( Reflection.findMethod(RocketComponent.class, "setName", String.class))); + setters.put("RocketComponent:id", new StringSetter( + Reflection.findMethod(RocketComponent.class, "setID", String.class))); setters.put("RocketComponent:color", new ColorSetter( Reflection.findMethod(RocketComponent.class, "setColor", Color.class))); setters.put("RocketComponent:linestyle", new EnumSetter( diff --git a/core/src/net/sf/openrocket/file/openrocket/importt/FlightDataBranchHandler.java b/core/src/net/sf/openrocket/file/openrocket/importt/FlightDataBranchHandler.java index d44303c9b..9641f13d6 100644 --- a/core/src/net/sf/openrocket/file/openrocket/importt/FlightDataBranchHandler.java +++ b/core/src/net/sf/openrocket/file/openrocket/importt/FlightDataBranchHandler.java @@ -8,6 +8,8 @@ import net.sf.openrocket.file.simplesax.AbstractElementHandler; import net.sf.openrocket.file.simplesax.ElementHandler; import net.sf.openrocket.file.simplesax.PlainTextHandler; import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.simulation.FlightDataBranch; import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.FlightEvent; @@ -126,6 +128,8 @@ class FlightDataBranchHandler extends AbstractElementHandler { if (element.equals("event")) { double time; FlightEvent.Type type; + String sourceID; + RocketComponent source = null; try { time = DocumentConfig.stringToDouble(attributes.get("time")); @@ -139,8 +143,15 @@ class FlightDataBranchHandler extends AbstractElementHandler { warnings.add("Illegal event specification, ignoring."); return; } + + // Get the event source + Rocket rocket = context.getOpenRocketDocument().getRocket(); + sourceID = attributes.get("source"); + if (sourceID != null) { + source = rocket.findComponent(sourceID); + } - branch.addEvent(new FlightEvent(type, time)); + branch.addEvent(new FlightEvent(type, time, source)); return; } diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/RocketComponentSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/RocketComponentSaver.java index 04d031cd9..0bc98071c 100644 --- a/core/src/net/sf/openrocket/file/openrocket/savers/RocketComponentSaver.java +++ b/core/src/net/sf/openrocket/file/openrocket/savers/RocketComponentSaver.java @@ -36,6 +36,7 @@ public class RocketComponentSaver { protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { elements.add("" + TextUtil.escapeXML(c.getName()) + ""); + elements.add("" + TextUtil.escapeXML(c.getID()) + ""); ComponentPreset preset = c.getPresetComponent(); if (preset != null) { diff --git a/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java b/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java index cee08ffee..46bb50035 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java +++ b/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java @@ -1282,8 +1282,16 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab mutex.verify(); this.id = UniqueID.uuid(); } - - + + /** + * Set the ID for this component. + * Generally not recommended to directly set the ID, this is done automatically. Only use this in case you have to. + * @param newID new ID + */ + public void setID(String newID) { + mutex.verify(); + this.id = newID; + } /** @@ -2047,6 +2055,16 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab } return children; } + + /** + * Checks whether this component contains as one of its (sub-)children. + * @param component component to check + * @return true if component is a (sub-)child of this component + */ + public final boolean containsChild(RocketComponent component) { + List allChildren = getAllChildren(); + return allChildren.contains(component); + } /** diff --git a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java index ffdb3c2fa..be9fd8bc6 100644 --- a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java +++ b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java @@ -489,7 +489,7 @@ public class BasicEventSimulationEngine implements SimulationEngine { SimulationStatus boosterStatus = new SimulationStatus(currentStatus); // Prepare the new simulation branch - boosterStatus.setFlightData(new FlightDataBranch(boosterStage.getName(), currentStatus.getFlightData())); + boosterStatus.setFlightData(new FlightDataBranch(boosterStage.getName(), boosterStage, currentStatus.getFlightData())); boosterStatus.getFlightData().addEvent(event); // Mark the current status as having dropped the current stage and all stages below it diff --git a/core/src/net/sf/openrocket/simulation/FlightData.java b/core/src/net/sf/openrocket/simulation/FlightData.java index 7a559b3f2..5d6f62017 100644 --- a/core/src/net/sf/openrocket/simulation/FlightData.java +++ b/core/src/net/sf/openrocket/simulation/FlightData.java @@ -134,8 +134,12 @@ public class FlightData { return branches.size(); } - public FlightDataBranch getBranch(int n) { - return branches.get(n); + public FlightDataBranch getBranch(int stageNr) { + return branches.get(stageNr); + } + + public int getStageNr(FlightDataBranch branch) { + return branches.indexOf(branch); } public List getBranches() { diff --git a/core/src/net/sf/openrocket/simulation/FlightDataBranch.java b/core/src/net/sf/openrocket/simulation/FlightDataBranch.java index e2547e4c4..395c96343 100644 --- a/core/src/net/sf/openrocket/simulation/FlightDataBranch.java +++ b/core/src/net/sf/openrocket/simulation/FlightDataBranch.java @@ -6,6 +6,9 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import net.sf.openrocket.rocketcomponent.AxialStage; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.util.ArrayList; import net.sf.openrocket.util.Monitorable; import net.sf.openrocket.util.Mutable; @@ -30,11 +33,10 @@ public class FlightDataBranch implements Monitorable { /** The name of this flight data branch. */ private final String branchName; - private final Map> values = - new LinkedHashMap>(); + private final Map> values = new LinkedHashMap<>(); - private final Map maxValues = new HashMap(); - private final Map minValues = new HashMap(); + private final Map maxValues = new HashMap<>(); + private final Map minValues = new HashMap<>(); /** * time for the rocket to reach apogee if the flight had been no recovery deployment @@ -77,23 +79,19 @@ public class FlightDataBranch implements Monitorable { } /** - * Make a flight data branch with one data point copied from its parent. Intended for use + * Make a flight data branch with all data points copied from its parent. Intended for use * when creating a new branch upon stage separation, so the data at separation is present * in both branches (and if the new branch has an immediate exception, it can be plotted) + * + * @param branchName the name of the new branch. + * @param srcComponent the component that is the source of the new branch. + * @param parent the parent branch to copy data from. */ - public FlightDataBranch(String branchName, FlightDataBranch parent) { + public FlightDataBranch(String branchName, RocketComponent srcComponent, FlightDataBranch parent) { this.branchName = branchName; - // need to have at least one type to set up values - values.put(FlightDataType.TYPE_TIME, new ArrayList()); - minValues.put(FlightDataType.TYPE_TIME, Double.NaN); - maxValues.put(FlightDataType.TYPE_TIME, Double.NaN); - - // copy all values into new FlightDataBranch - this.addPoint(); - for (FlightDataType t : parent.getTypes()) { - this.setValue(t, parent.getLast(t)); - } + // Copy all the values from the parent + copyValuesFromBranch(parent, srcComponent); } /** @@ -115,12 +113,27 @@ public class FlightDataBranch implements Monitorable { public void addPoint() { mutable.check(); - for (FlightDataType t : values.keySet()) { - values.get(t).add(Double.NaN); + for (FlightDataType type : values.keySet()) { + sanityCheckValues(type, Double.NaN); + values.get(type).add(Double.NaN); } modID++; } - + + private void sanityCheckValues(FlightDataType type, Double value) { + ArrayList list = values.get(type); + + if (list == null) { + list = new ArrayList<>(); + int n = getLength(); + for (int i = 0; i < n; i++) { + list.add(Double.NaN); + } + values.put(type, list); + minValues.put(type, value); + maxValues.put(type, value); + } + } /** * Set the value for a specific data type at the latest point. New variable types can be @@ -132,20 +145,10 @@ public class FlightDataBranch implements Monitorable { */ public void setValue(FlightDataType type, double value) { mutable.check(); - + + sanityCheckValues(type, value); ArrayList list = values.get(type); - if (list == null) { - list = new ArrayList(); - int n = getLength(); - for (int i = 0; i < n; i++) { - list.add(Double.NaN); - } - values.put(type, list); - minValues.put(type, value); - maxValues.put(type, value); - } - if (list.size() > 0) { list.set(list.size() - 1, value); } @@ -161,7 +164,68 @@ public class FlightDataBranch implements Monitorable { } modID++; } - + + /** + * Clears all the current values in the branch and copies the values from the given branch. + * @param srcBranch the branch to copy values from + * @param srcComponent the component that is the source of this branch (used for copying events) + */ + private void copyValuesFromBranch(FlightDataBranch srcBranch, RocketComponent srcComponent) { + this.values.clear(); + + // Need to have at least one type to set up values + values.put(FlightDataType.TYPE_TIME, new ArrayList<>()); + minValues.put(FlightDataType.TYPE_TIME, Double.NaN); + maxValues.put(FlightDataType.TYPE_TIME, Double.NaN); + + if (srcBranch == null) { + return; + } + + // Copy flight data + for (int i = 0; i < srcBranch.getLength(); i++) { + this.addPoint(); + for (FlightDataType type : srcBranch.getTypes()) { + this.setValue(type, srcBranch.getByIndex(type, i)); + } + } + + // Copy flight events belonging to this branch + List sustainerEvents = srcBranch.getEvents(); + for (FlightEvent event : sustainerEvents) { + // Stage separation is already added elsewhere, so don't copy it over (otherwise you have a duplicate) + if (event.getType() == FlightEvent.Type.STAGE_SEPARATION) { + continue; + } + RocketComponent srcEventComponent = event.getSource(); + // Ignore null events + if (srcComponent == null || srcEventComponent == null) { + continue; + } + // Ignore events from other stages. Important for when the current stage has a booster stage; we don't want to copy over the booster events. + if (getStageForComponent(srcComponent) != getStageForComponent(srcEventComponent)) { + continue; + } + if (srcComponent == srcEventComponent || srcComponent.containsChild(srcEventComponent)) { + events.add(event); + } + } + } + + /** + * A safer method for checking the stage of a component (that shouldn't throw exceptions when calling on stages/rockets) + * @param component the component to get the stage of + * @return the stage of the component, or null if the component is a rocket + */ + private AxialStage getStageForComponent(RocketComponent component) { + if (component instanceof AxialStage) { + return (AxialStage) component; + } else if (component instanceof Rocket) { + return null; + } else { + return component.getStage(); + } + } /** * Return the branch name. @@ -203,6 +267,23 @@ public class FlightDataBranch implements Monitorable { return null; return list.clone(); } + + /** + * Return the value of the specified type at the specified index. + * @param type the variable type + * @param index the data index of the value + * @return the value at the specified index + */ + public Double getByIndex(FlightDataType type, int index) { + if (index < 0 || index >= getLength()) { + throw new IllegalArgumentException("Index out of bounds"); + } + ArrayList list = values.get(type); + if (list == null) { + return null; + } + return list.get(index); + } /** * Return the last value of the specified type in the branch, or NaN if the type is diff --git a/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java b/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java index 0eae42619..d7ccef0fb 100644 --- a/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java +++ b/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java @@ -331,9 +331,9 @@ public class OpenRocketSaverTest { //////////////////////////////// @Test - public void testFileVersion108_withSimulationExtension() { + public void testFileVersion109_withSimulationExtension() { OpenRocketDocument rocketDoc = TestRockets.makeTestRocket_v107_withSimulationExtension(SIMULATION_EXTENSION_SCRIPT); - assertEquals(108, getCalculatedFileVersion(rocketDoc)); + assertEquals(109, getCalculatedFileVersion(rocketDoc)); } diff --git a/core/test/net/sf/openrocket/simulation/FlightEventsTest.java b/core/test/net/sf/openrocket/simulation/FlightEventsTest.java index e4fa4d305..5340eefc0 100644 --- a/core/test/net/sf/openrocket/simulation/FlightEventsTest.java +++ b/core/test/net/sf/openrocket/simulation/FlightEventsTest.java @@ -108,8 +108,8 @@ public class FlightEventsTest extends BaseTestCase { // events whose time is too variable to check are given a time of 1200 for (int b = 0; b < 3; b++) { FlightEvent[] expectedEvents; - final RocketComponent[] expectedSources; switch (b) { + // Sustainer (payload fairing stage) case 0: expectedEvents = new FlightEvent[] { new FlightEvent(FlightEvent.Type.LAUNCH, 0.0, rocket), @@ -129,15 +129,23 @@ public class FlightEventsTest extends BaseTestCase { new FlightEvent(FlightEvent.Type.SIMULATION_END, 1200, null) }; break; + // Core stage case 1: expectedEvents = new FlightEvent[] { + new FlightEvent(FlightEvent.Type.IGNITION, 0.0, coreBody), + new FlightEvent(FlightEvent.Type.BURNOUT, 2.0, coreBody), + new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 2.0, coreStage), new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 2.0, coreStage), new FlightEvent(FlightEvent.Type.GROUND_HIT, 1200, null), new FlightEvent(FlightEvent.Type.SIMULATION_END, 1200, null) }; break; + // Booster stage case 2: expectedEvents = new FlightEvent[] { + new FlightEvent(FlightEvent.Type.IGNITION, 0.0, boosterMotorTubes), + new FlightEvent(FlightEvent.Type.BURNOUT, 2.0, boosterMotorTubes), + new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 2.0, boosterStage), new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 2.0, boosterStage), new FlightEvent(FlightEvent.Type.TUMBLE, 3.551, null), new FlightEvent(FlightEvent.Type.GROUND_HIT, 1200, null), @@ -150,10 +158,7 @@ public class FlightEventsTest extends BaseTestCase { // Test event count final FlightDataBranch branch = sim.getSimulatedData().getBranch(b); - final FlightEvent[] events = (FlightEvent[]) branch.getEvents().toArray(new FlightEvent[0]); - for (int i = 0; i < events.length; i++) { - System.out.println("branch " + b + " index " + i + " event " + events[i]); - } + final FlightEvent[] events = branch.getEvents().toArray(new FlightEvent[0]); assertEquals(" Multi-stage simulation, branch " + b + " invalid number of events ", expectedEvents.length, events.length); // Test that all expected events are present, in the right order, at the right time, from the right sources diff --git a/fileformat.txt b/fileformat.txt index dafe8745a..aa3093182 100644 --- a/fileformat.txt +++ b/fileformat.txt @@ -63,3 +63,6 @@ The following file format versions exist: Rename to ( remains for backward compatibility) Rename to ( remains for backward compatibility) Rename to ( remains for backward compatibility) + +1.9: Introduced with OpenRocket 23.xx. + Added ID for each rocket component, to in turn add this ID as a source for flight events. \ No newline at end of file diff --git a/swing/src/net/sf/openrocket/gui/plot/SimulationPlot.java b/swing/src/net/sf/openrocket/gui/plot/SimulationPlot.java index 57f45c09c..e30b01646 100644 --- a/swing/src/net/sf/openrocket/gui/plot/SimulationPlot.java +++ b/swing/src/net/sf/openrocket/gui/plot/SimulationPlot.java @@ -192,9 +192,8 @@ public class SimulationPlot { } data[axis].addSeries(series); } - // For each of the secondary branches, we use data from branch 0 for the earlier times + // Secondary branches for (int branchIndex = 1; branchIndex < branchCount; branchIndex++) { - FlightDataBranch primaryBranch = simulation.getSimulatedData().getBranch(0); FlightDataBranch thisBranch = simulation.getSimulatedData().getBranch(branchIndex); // Ignore empty branches @@ -206,25 +205,10 @@ public class SimulationPlot { continue; } - // Get first time index used in secondary branch; - double firstSampleTime = thisBranch.get(FlightDataType.TYPE_TIME).get(0); - XYSeries series = new XYSeries(seriesCount++, false, true); series.setDescription(thisBranch.getBranchName() + ": " + name); - // Copy the first points from the primaryBranch. - List primaryT = primaryBranch.get(FlightDataType.TYPE_TIME); - List primaryx = primaryBranch.get(domainType); - List primaryy = primaryBranch.get(type); - - for (int j = 0; j < primaryT.size(); j++) { - if (primaryT.get(j) >= firstSampleTime) { - break; - } - series.add(domainUnit.toUnit(primaryx.get(j)), unit.toUnit(primaryy.get(j))); - } - - // Now copy all the data from the secondary branch + // Copy all the data from the secondary branch List plotx = thisBranch.get(domainType); List ploty = thisBranch.get(type);