diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties
index 32b976d38..5b77436bb 100644
--- a/core/resources/l10n/messages.properties
+++ b/core/resources/l10n/messages.properties
@@ -95,7 +95,10 @@ BasicFrame.WarningDialog.title = Warnings while opening file
BasicFrame.WarningDialog.saving.title = Warnings while opening file
BasicFrame.ErrorWarningDialog.txt1 = Please correct the errors.
BasicFrame.ErrorWarningDialog.saving.title = Errors/Warnings while saving file
+BasicFrame.lbl.SaveRocketInfo = Save Design Info
+! SaveDesignInfoPanel
+SaveDesignInfoPanel.lbl.FillInInfo = (Optional) Fill in the design information for this file
! General error messages used in multiple contexts
error.fileExists.title = File exists
@@ -582,12 +585,13 @@ simpanel.but.ttip.editsim = Edit the selected simulation
simpanel.but.ttip.runsimu = Re-run the selected simulations
simpanel.but.ttip.deletesim = Delete the selected simulations
simpanel.pop.edit = Edit
+simpanel.pop.copyValues = Copy values
simpanel.pop.plot = Plot / Export
simpanel.pop.run = Run
simpanel.pop.delete = Delete
simpanel.pop.duplicate = Duplicate
simpanel.pop.exportSimTableToCSV = Export simulation table as CSV file
-simpanel.pop.exportSelectedSimsToCSV = Export simulations as CSV file
+simpanel.pop.exportSelectedSimsToCSV = Export simulation(s) as CSV file
simpanel.pop.exportToCSV.save.dialog.title = Save as CSV file
simpanel.dlg.no.simulation.table.rows = Simulation table has no entries\u2026 Please run a simulation first.
simpanel.checkbox.donotask = Do not ask me again
@@ -595,6 +599,7 @@ simpanel.lbl.defpref = You can change the default operation in the preferences.
simpanel.dlg.lbl.DeleteSim1 = Delete the selected simulations?
simpanel.dlg.lbl.DeleteSim2 = This operation cannot be undone.
simpanel.dlg.lbl.DeleteSim3 = Delete simulations
+simpanel.col.Status = Status
simpanel.col.Name = Name
simpanel.col.Motors = Motors
simpanel.col.Configuration = Configuration
@@ -640,6 +645,8 @@ RK4SimulationStepper.error.valuesTooLarge = Simulation values exceeded limits.
SimulationModifierTree.OptimizationParameters = Optimization Parameters
+SimulationStepper.error.totalMassZero = Total mass of active states is 0
+
! SimulationExportPanel
SimExpPan.border.Vartoexport = Variables to export
SimExpPan.border.Stage = Stage to export
@@ -1191,6 +1198,7 @@ InnerTubeCfg.tab.ttip.Radialpos = Radial position
InnerTubeCfg.lbl.Selectclustercfg = Select cluster configuration:
InnerTubeCfg.lbl.TubeSep = Tube separation:
InnerTubeCfg.lbl.ttip.TubeSep = The separation of the tubes, 1.0 = touching each other
+InnerTubeCfg.lbl.ttip.TubeSepAbs = The separation of the tubes, 0 = touching each other
InnerTubeCfg.lbl.Rotation = Rotation:
InnerTubeCfg.lbl.ttip.Rotation = Rotation angle of the cluster configuration
InnerTubeCfg.lbl.Rotangle = Rotation angle of the cluster configuration
@@ -1199,6 +1207,10 @@ InnerTubeCfg.lbl.longA1 = Split the cluster into separate components.
InnerTubeCfg.lbl.longA2 = This also duplicates all components attached to this inner tube.
InnerTubeCfg.but.Resetsettings = Reset settings
InnerTubeCfg.but.ttip.Resetsettings = Reset the separation and rotation to the default values
+InnerTubeCfg.radioBut.Relative = Relative
+InnerTubeCfg.radioBut.Relative.ttip = The separation is measured relative to the outer diameter of the inner tube
+InnerTubeCfg.radioBut.Absolute = Absolute
+InnerTubeCfg.radioBut.Absolute.ttip = The separation is measured in length units
! LaunchLugConfig
LaunchLugCfg.lbl.Length = Length:
@@ -2022,6 +2034,7 @@ Warning.TUBE_SEPARATION = Space between tube fins may not simulate accurately.
Warning.TUBE_OVERLAP = Overlapping tube fins may not simulate accurately.
Warning.EMPTY_BRANCH = Simulation branch contains no data
Warning.SEPARATION_ORDER = Stages separated in an unreasonable order
+Warning.EARLY_SEPARATION = Stages separated before clearing launch rod/rail
! Scale dialog
ScaleDialog.lbl.scaleRocket = Entire rocket
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/logging/Warning.java b/core/src/net/sf/openrocket/logging/Warning.java
index 9d1bc0421..23c4bcb90 100644
--- a/core/src/net/sf/openrocket/logging/Warning.java
+++ b/core/src/net/sf/openrocket/logging/Warning.java
@@ -386,7 +386,11 @@ public abstract class Warning extends Message {
public static final Warning TUBE_SEPARATION = new Other(trans.get("Warning.TUBE_SEPARATION"));
public static final Warning TUBE_OVERLAP = new Other(trans.get("Warning.TUBE_OVERLAP"));
+ /** A Warning that stage separation occurred at other than the last stage */
public static final Warning SEPARATION_ORDER = new Other(trans.get("Warning.SEPARATION_ORDER"));
+ /** A Warning that stage separation occurred before the rocket cleared the launch rod or rail */
+ public static final Warning EARLY_SEPARATION = new Other(trans.get("Warning.EARLY_SEPARATION"));
+
public static final Warning EMPTY_BRANCH = new Other(trans.get("Warning.EMPTY_BRANCH"));
}
diff --git a/core/src/net/sf/openrocket/rocketcomponent/AxialStage.java b/core/src/net/sf/openrocket/rocketcomponent/AxialStage.java
index 504969e27..249f8dfeb 100644
--- a/core/src/net/sf/openrocket/rocketcomponent/AxialStage.java
+++ b/core/src/net/sf/openrocket/rocketcomponent/AxialStage.java
@@ -170,14 +170,14 @@ public class AxialStage extends ComponentAssembly implements FlightConfigurableC
* @return the previous stage in the rocket
*/
public AxialStage getUpperStage() {
- if( null == this.parent ) {
+ if (this.parent == null) {
return null;
- }else if(Rocket.class.isAssignableFrom(this.parent.getClass()) ){
- final int thisIndex = getStageNumber();
- if( 0 < thisIndex ){
- return (AxialStage)parent.getChild(thisIndex-1);
+ } else if (Rocket.class.isAssignableFrom(this.parent.getClass())) {
+ final int thisIndex = parent.getChildPosition(this);
+ if (thisIndex > 0) {
+ return (AxialStage) parent.getChild(thisIndex-1);
}
- }else {
+ } else {
return this.parent.getStage();
}
return null;
@@ -207,10 +207,10 @@ public class AxialStage extends ComponentAssembly implements FlightConfigurableC
public StageSeparationConfiguration getSeparationConfiguration() {
FlightConfiguration flConfig = getRocket().getSelectedConfiguration();
StageSeparationConfiguration sepConfig = getSeparationConfigurations().get(flConfig.getId());
- // to ensure the configuration is distinct, and we're not modifying the default
+ // To ensure the configuration is distinct, and we're not modifying the default
if ((sepConfig == getSeparationConfigurations().getDefault())
&& (flConfig.getId() != FlightConfigurationId.DEFAULT_VALUE_FCID)) {
- sepConfig = new StageSeparationConfiguration();
+ sepConfig = sepConfig.copy(flConfig.getId());
getSeparationConfigurations().set(flConfig.getId(), sepConfig);
}
return sepConfig;
diff --git a/core/src/net/sf/openrocket/rocketcomponent/InnerTube.java b/core/src/net/sf/openrocket/rocketcomponent/InnerTube.java
index 1e17bb335..e9477baf2 100644
--- a/core/src/net/sf/openrocket/rocketcomponent/InnerTube.java
+++ b/core/src/net/sf/openrocket/rocketcomponent/InnerTube.java
@@ -170,6 +170,11 @@ public class InnerTube extends ThicknessRingComponent implements AxialPositionab
" Please set setClusterConfiguration(ClusterConfiguration) instead.",
new UnsupportedOperationException("InnerTube.setInstanceCount(..) on an"+this.getClass().getSimpleName()));
}
+
+ @Override
+ public boolean isAfter(){
+ return false;
+ }
/**
* Get the cluster scaling. A value of 1.0 indicates that the tubes are packed
@@ -177,14 +182,10 @@ public class InnerTube extends ThicknessRingComponent implements AxialPositionab
* pack inside each other.
*/
public double getClusterScale() {
+ mutex.verify();
return clusterScale;
}
- @Override
- public boolean isAfter(){
- return false;
- }
-
/**
* Set the cluster scaling.
* @see #getClusterScale()
@@ -203,6 +204,23 @@ public class InnerTube extends ThicknessRingComponent implements AxialPositionab
clusterScale = scale;
fireComponentChangeEvent(new ComponentChangeEvent(this, ComponentChangeEvent.MASS_CHANGE));
}
+
+ /**
+ * Get the cluster scaling as an absolute distance measurement. A value of 0 indicates that the tubes are packed
+ * touching each other, larger values separate the tubes and smaller values pack inside each other.
+ */
+ public double getClusterScaleAbsolute() {
+ return (getClusterScale() - 1) * getOuterRadius() * 2;
+ }
+
+ /**
+ * Set the absolute cluster scaling (in terms of distance).
+ * @see #getClusterScaleAbsolute()
+ */
+ public void setClusterScaleAbsolute(double scale) {
+ double scaleRel = scale / (getOuterRadius() * 2) + 1;
+ setClusterScale(scaleRel);
+ }
diff --git a/core/src/net/sf/openrocket/rocketcomponent/MassObject.java b/core/src/net/sf/openrocket/rocketcomponent/MassObject.java
index d370e3b8e..63e905e4d 100644
--- a/core/src/net/sf/openrocket/rocketcomponent/MassObject.java
+++ b/core/src/net/sf/openrocket/rocketcomponent/MassObject.java
@@ -109,7 +109,7 @@ public abstract class MassObject extends InternalComponent {
return radius;
}
if (parent instanceof NoseCone) {
- return ((NoseCone) parent).getAftRadius();
+ return ((NoseCone) parent).getBaseRadius();
} else if (parent instanceof Transition) {
double foreRadius = ((Transition) parent).getForeRadius();
double aftRadius = ((Transition) parent).getAftRadius();
diff --git a/core/src/net/sf/openrocket/rocketcomponent/Parachute.java b/core/src/net/sf/openrocket/rocketcomponent/Parachute.java
index e62158f90..5111b13af 100644
--- a/core/src/net/sf/openrocket/rocketcomponent/Parachute.java
+++ b/core/src/net/sf/openrocket/rocketcomponent/Parachute.java
@@ -211,14 +211,15 @@ public class Parachute extends RecoveryDevice {
// // Set preset parachute packed length
if ((preset.has(ComponentPreset.PACKED_LENGTH)) && preset.get(ComponentPreset.PACKED_LENGTH) > 0) {
- length = preset.get(ComponentPreset.PACKED_LENGTH);
+ setLength(preset.get(ComponentPreset.PACKED_LENGTH));
}
// // Set preset parachute packed diameter
if ((preset.has(ComponentPreset.PACKED_DIAMETER)) && preset.get(ComponentPreset.PACKED_DIAMETER) > 0) {
- radius = preset.get(ComponentPreset.PACKED_DIAMETER) / 2;
+ setRadius(preset.get(ComponentPreset.PACKED_DIAMETER) / 2);
}
// // Size parachute packed diameter within parent inner diameter
- if (length > 0 && radius > 0) {
+ if (preset.has(ComponentPreset.PACKED_LENGTH) && (getLength() > 0) &&
+ preset.has(ComponentPreset.PACKED_DIAMETER) && (getRadius() > 0)) {
setRadiusAutomatic(true);
}
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/AbstractEulerStepper.java b/core/src/net/sf/openrocket/simulation/AbstractEulerStepper.java
index a004b1069..7f448202b 100644
--- a/core/src/net/sf/openrocket/simulation/AbstractEulerStepper.java
+++ b/core/src/net/sf/openrocket/simulation/AbstractEulerStepper.java
@@ -3,10 +3,12 @@ package net.sf.openrocket.simulation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.models.atmosphere.AtmosphericConditions;
import net.sf.openrocket.rocketcomponent.InstanceMap;
import net.sf.openrocket.rocketcomponent.RecoveryDevice;
import net.sf.openrocket.simulation.exception.SimulationException;
+import net.sf.openrocket.startup.Application;
import net.sf.openrocket.util.Coordinate;
import net.sf.openrocket.util.GeodeticComputationStrategy;
import net.sf.openrocket.util.MathUtil;
@@ -14,6 +16,7 @@ import net.sf.openrocket.util.WorldCoordinate;
public abstract class AbstractEulerStepper extends AbstractSimulationStepper {
private static final Logger log = LoggerFactory.getLogger(AbstractEulerStepper.class);
+ private static final Translator trans = Application.getTranslator();
private static final double RECOVERY_TIME_STEP = 0.5;
@@ -46,12 +49,15 @@ public abstract class AbstractEulerStepper extends AbstractSimulationStepper {
double dynP = (0.5 * atmosphere.getDensity() * airSpeed.length2());
double dragForce = getCD() * dynP * status.getConfiguration().getReferenceArea();
- // n.b. this is constant, and could be calculated once at the beginning of this simulation branch...
double rocketMass = calculateStructureMass(status).getMass();
double motorMass = calculateMotorMass(status).getMass();
double mass = rocketMass + motorMass;
+ if (mass < MathUtil.EPSILON) {
+ throw new SimulationException(trans.get("SimulationStepper.error.totalMassZero"));
+ }
+
// Compute drag acceleration
Coordinate linearAcceleration;
if (airSpeed.length() > 0.001) {
diff --git a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java
index ffdb3c2fa..32977c194 100644
--- a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java
+++ b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java
@@ -485,11 +485,16 @@ public class BasicEventSimulationEngine implements SimulationEngine {
currentStatus.getWarnings().add(Warning.SEPARATION_ORDER);
}
+ // If I haven't cleared the rail yet, flag a warning
+ if (!currentStatus.isLaunchRodCleared()) {
+ currentStatus.getWarnings().add(Warning.EARLY_SEPARATION);
+ }
+
// Create a new simulation branch for the booster
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/src/net/sf/openrocket/simulation/RK4SimulationStepper.java b/core/src/net/sf/openrocket/simulation/RK4SimulationStepper.java
index 2bfa14b66..33ff68d69 100644
--- a/core/src/net/sf/openrocket/simulation/RK4SimulationStepper.java
+++ b/core/src/net/sf/openrocket/simulation/RK4SimulationStepper.java
@@ -329,6 +329,10 @@ public class RK4SimulationStepper extends AbstractSimulationStepper {
store.motorMass = calculateMotorMass(status);
store.rocketMass = structureMassData.add( store.motorMass );
+ if (store.rocketMass.getMass() < MathUtil.EPSILON) {
+ throw new SimulationException(trans.get("SimulationStepper.error.totalMassZero"));
+ }
+
// Calculate the forces from the aerodynamic coefficients
double dynP = (0.5 * store.flightConditions.getAtmosphericConditions().getDensity() *
diff --git a/core/src/net/sf/openrocket/startup/Preferences.java b/core/src/net/sf/openrocket/startup/Preferences.java
index 78e87293d..4c4f60ef0 100644
--- a/core/src/net/sf/openrocket/startup/Preferences.java
+++ b/core/src/net/sf/openrocket/startup/Preferences.java
@@ -82,6 +82,7 @@ public abstract class Preferences implements ChangeSource {
private static final String AUTO_OPEN_LAST_DESIGN = "AutoOpenLastDesign";
private static final String OPEN_LEFTMOST_DESIGN_TAB = "OpenLeftmostDesignTab";
private static final String SHOW_DISCARD_CONFIRMATION = "IgnoreDiscardEditingWarning";
+ private static final String SHOW_SAVE_ROCKET_INFO = "ShowSaveRocketInfo";
private static final String SHOW_DISCARD_SIMULATION_CONFIRMATION = "IgnoreDiscardSimulationEditingWarning";
private static final String SHOW_DISCARD_PREFERENCES_CONFIRMATION = "IgnoreDiscardPreferencesWarning";
public static final String MARKER_STYLE_ICON = "MarkerStyleIcon";
@@ -589,6 +590,21 @@ public abstract class Preferences implements ChangeSource {
this.putBoolean(SHOW_DISCARD_CONFIRMATION, enabled);
}
+ /**
+ * Returns whether a 'save rocket information' dialog should be shown after saving a new design file.
+ * @return true if the 'save rocket information' dialog should be shown.
+ */
+ public final boolean isShowSaveRocketInfo() {
+ return this.getBoolean(SHOW_SAVE_ROCKET_INFO, true);
+ }
+
+ /**
+ * Enable/Disable showing a 'save rocket information' dialog after saving a new design file.
+ * @return true if the 'save rocket information' dialog should be shown.
+ */
+ public final void setShowSaveRocketInfo(boolean enabled) {
+ this.putBoolean(SHOW_SAVE_ROCKET_INFO, enabled);
+ }
/**
* Answer if a confirmation dialog should be shown when canceling a simulation config operation.
*
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/resources/datafiles/examples/Two-stage rocket.ork b/swing/resources/datafiles/examples/Two-stage rocket.ork
index 603c3a4e0..66d868c15 100644
Binary files a/swing/resources/datafiles/examples/Two-stage rocket.ork and b/swing/resources/datafiles/examples/Two-stage rocket.ork differ
diff --git a/swing/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java
index 4ac778c4e..320f97139 100644
--- a/swing/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java
+++ b/swing/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java
@@ -9,6 +9,8 @@ import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.Ellipse2D;
@@ -17,12 +19,14 @@ import java.util.EventObject;
import java.util.List;
import javax.swing.BorderFactory;
+import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
+import javax.swing.JRadioButton;
import javax.swing.JSpinner;
import javax.swing.SwingUtilities;
import javax.swing.border.BevelBorder;
@@ -47,6 +51,7 @@ import net.sf.openrocket.rocketcomponent.MotorMount;
import net.sf.openrocket.rocketcomponent.RingComponent;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.startup.Preferences;
import net.sf.openrocket.unit.UnitGroup;
import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.Coordinate;
@@ -56,6 +61,9 @@ import net.sf.openrocket.util.StateChangeListener;
public class InnerTubeConfig extends RocketComponentConfig {
private static final long serialVersionUID = 7900041420864324470L;
private static final Translator trans = Application.getTranslator();
+ private static final Preferences prefs = Application.getPreferences();
+
+ private static final String PREF_SEPARATION_RELATIVE = "InnerTubeSeparationRelative";
public InnerTubeConfig(OpenRocketDocument d, RocketComponent c, JDialog parent) {
@@ -280,29 +288,88 @@ public class InnerTubeConfig extends RocketComponentConfig {
//// The separation of the tubes, 1.0 = touching each other
l.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.TubeSep"));
subPanel.add(l);
- DoubleModel dm = new DoubleModel(component, "ClusterScale", 1, UnitGroup.UNITS_NONE, 0);
- JSpinner spin = new JSpinner(dm.getSpinnerModel());
- spin.setEditor(new SpinnerEditor(spin));
- //// The separation of the tubes, 1.0 = touching each other
- spin.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.TubeSep"));
- subPanel.add(spin, "growx");
- order.add(((SpinnerEditor) spin.getEditor()).getTextField());
+ //// Models
+ final boolean useRelativeSeparation = prefs.getBoolean(PREF_SEPARATION_RELATIVE, true);
+ final DoubleModel clusterScaleModelRel = new DoubleModel(component, "ClusterScale", 1, UnitGroup.UNITS_NONE, 0);
+ final DoubleModel clusterScaleModelAbs = new DoubleModel(component, "ClusterScaleAbsolute", 1, UnitGroup.UNITS_LENGTH);
+ final DoubleModel clusterScaleModel = useRelativeSeparation ? clusterScaleModelRel : clusterScaleModelAbs;
- BasicSlider bs = new BasicSlider(dm.getSliderModel(0, 1, 4));
- //// The separation of the tubes, 1.0 = touching each other
- bs.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.TubeSep"));
- subPanel.add(bs, "skip,w 100lp, wrap");
+ final String clusterScaleTtipRel = trans.get("InnerTubeCfg.lbl.ttip.TubeSep");
+ final String clusterScaleTtipAbs = trans.get("InnerTubeCfg.lbl.ttip.TubeSepAbs");
+ final String clusterScaleTtip = useRelativeSeparation ? clusterScaleTtipRel : clusterScaleTtipAbs;
+
+ JSpinner clusterScaleSpin = new JSpinner(clusterScaleModel.getSpinnerModel());
+ clusterScaleSpin.setEditor(new SpinnerEditor(clusterScaleSpin));
+ clusterScaleSpin.setToolTipText(clusterScaleTtip);
+ subPanel.add(clusterScaleSpin, "growx");
+ order.add(((SpinnerEditor) clusterScaleSpin.getEditor()).getTextField());
+
+ UnitSelector clusterScaleUnit = new UnitSelector(clusterScaleModel);
+ subPanel.add(clusterScaleUnit, "growx");
+
+ BasicSlider clusterScaleBs = new BasicSlider(clusterScaleModel.getSliderModel(0, 1, 4));
+ subPanel.add(clusterScaleBs, "w 100lp, wrap");
+
+ // Relative/absolute separation
+ JRadioButton rbRel = new JRadioButton(trans.get("InnerTubeCfg.radioBut.Relative"));
+ JRadioButton rbAbs = new JRadioButton(trans.get("InnerTubeCfg.radioBut.Absolute"));
+ rbRel.setToolTipText(trans.get("InnerTubeCfg.radioBut.Relative.ttip"));
+ rbAbs.setToolTipText(trans.get("InnerTubeCfg.radioBut.Absolute.ttip"));
+ ButtonGroup bg = new ButtonGroup();
+ bg.add(rbRel);
+ bg.add(rbAbs);
+ subPanel.add(rbRel, "skip, spanx, split 2");
+ subPanel.add(rbAbs, "wrap");
+
+ rbRel.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ if (e.getStateChange() == ItemEvent.DESELECTED)
+ return;
+ clusterScaleSpin.setModel(clusterScaleModelRel.getSpinnerModel());
+ clusterScaleSpin.setEditor(new SpinnerEditor(clusterScaleSpin));
+ clusterScaleUnit.setModel(clusterScaleModelRel);
+ clusterScaleBs.setModel(clusterScaleModelRel.getSliderModel(0, 1, 4));
+ clusterScaleSpin.setToolTipText(clusterScaleTtipRel);
+
+ prefs.putBoolean(PREF_SEPARATION_RELATIVE, false);
+ }
+ });
+ rbAbs.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ if (e.getStateChange() == ItemEvent.DESELECTED)
+ return;
+ DoubleModel radiusModelMin = new DoubleModel(component, "OuterRadius", -2, UnitGroup.UNITS_LENGTH);
+ DoubleModel radiusModelMax = new DoubleModel(component, "OuterRadius", 6, UnitGroup.UNITS_LENGTH);
+
+ clusterScaleSpin.setModel(clusterScaleModelAbs.getSpinnerModel());
+ clusterScaleSpin.setEditor(new SpinnerEditor(clusterScaleSpin));
+ clusterScaleUnit.setModel(clusterScaleModelAbs);
+ clusterScaleBs.setModel(clusterScaleModelAbs.getSliderModel(radiusModelMin, radiusModelMax));
+ clusterScaleSpin.setToolTipText(clusterScaleTtipAbs);
+
+ prefs.putBoolean(PREF_SEPARATION_RELATIVE, false);
+ }
+ });
+
+ // Select the button by default
+ if (prefs.getBoolean(PREF_SEPARATION_RELATIVE, true)) {
+ rbRel.setSelected(true);
+ } else {
+ rbAbs.setSelected(true);
+ }
// Rotation:
l = new JLabel(trans.get("InnerTubeCfg.lbl.Rotation"));
//// Rotation angle of the cluster configuration
l.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.Rotation"));
subPanel.add(l);
- dm = new DoubleModel(component, "ClusterRotation", 1, UnitGroup.UNITS_ANGLE,
+ DoubleModel dm = new DoubleModel(component, "ClusterRotation", 1, UnitGroup.UNITS_ANGLE,
-Math.PI, Math.PI);
- spin = new JSpinner(dm.getSpinnerModel());
+ JSpinner spin = new JSpinner(dm.getSpinnerModel());
spin.setEditor(new SpinnerEditor(spin));
//// Rotation angle of the cluster configuration
spin.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.Rotation"));
@@ -310,7 +377,7 @@ public class InnerTubeConfig extends RocketComponentConfig {
order.add(((SpinnerEditor) spin.getEditor()).getTextField());
subPanel.add(new UnitSelector(dm), "growx");
- bs = new BasicSlider(dm.getSliderModel());
+ BasicSlider bs = new BasicSlider(dm.getSliderModel());
//// Rotation angle of the cluster configuration
bs.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.Rotation"));
subPanel.add(bs, "w 100lp, wrap para");
diff --git a/swing/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java
index 487aa5a55..6a8dad3ef 100644
--- a/swing/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java
+++ b/swing/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java
@@ -69,7 +69,7 @@ public class RocketComponentConfig extends JPanel {
protected final OpenRocketDocument document;
protected final RocketComponent component;
protected final JTabbedPane tabbedPane;
- protected final ComponentConfigDialog parent;
+ protected final JDialog parent;
protected boolean isNewComponent = false; // Checks whether this config dialog is editing an existing component, or a new one
private final List invalidatables = new ArrayList();
@@ -86,7 +86,7 @@ public class RocketComponentConfig extends JPanel {
private DescriptionArea componentInfo;
private IconToggleButton infoBtn;
- private JPanel buttonPanel;
+ protected JPanel buttonPanel;
protected JButton okButton;
protected JButton cancelButton;
private AppearancePanel appearancePanel = null;
@@ -102,11 +102,7 @@ public class RocketComponentConfig extends JPanel {
this.document = document;
this.component = component;
- if (parent instanceof ComponentConfigDialog) {
- this.parent = (ComponentConfigDialog) parent;
- } else {
- this.parent = null;
- }
+ this.parent = parent;
// Check the listeners for the same type and massive status
allSameType = true;
@@ -194,7 +190,7 @@ public class RocketComponentConfig extends JPanel {
/**
* Add a section to the component configuration dialog that displays information about the component.
*/
- private void addComponentInfo(JPanel buttonPanel) {
+ protected void addComponentInfo(JPanel buttonPanel) {
// Don't add the info panel if this is a multi-comp edit
List listeners = component.getConfigListeners();
if (listeners != null && listeners.size() > 0) {
@@ -273,14 +269,14 @@ public class RocketComponentConfig extends JPanel {
@Override
public void actionPerformed(ActionEvent arg0) {
// Don't do anything on cancel if you are editing an existing component, and it is not modified
- if (!isNewComponent && parent != null && !parent.isModified()) {
- ComponentConfigDialog.disposeDialog();
+ if (!isNewComponent && parent != null && (parent instanceof ComponentConfigDialog && !((ComponentConfigDialog) parent).isModified())) {
+ disposeDialog();
return;
}
// Apply the cancel operation if set to auto discard in preferences
if (!preferences.isShowDiscardConfirmation()) {
ComponentConfigDialog.clearConfigListeners = false; // Undo action => config listeners of new component will be cleared
- ComponentConfigDialog.disposeDialog();
+ disposeDialog();
document.undo();
return;
}
@@ -291,7 +287,7 @@ public class RocketComponentConfig extends JPanel {
trans.get("RocketCompCfg.CancelOperation.title"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
if (resultYesNo == JOptionPane.YES_OPTION) {
ComponentConfigDialog.clearConfigListeners = false; // Undo action => config listeners of new component will be cleared
- ComponentConfigDialog.disposeDialog();
+ disposeDialog();
document.undo();
}
}
@@ -304,7 +300,7 @@ public class RocketComponentConfig extends JPanel {
okButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
- ComponentConfigDialog.disposeDialog();
+ disposeDialog();
}
});
buttonPanel.add(okButton);
@@ -314,7 +310,17 @@ public class RocketComponentConfig extends JPanel {
this.add(buttonPanel, "newline, spanx, growx");
}
- private JPanel createCancelOperationContent() {
+ protected void disposeDialog() {
+ if (parent != null) {
+ if (parent instanceof ComponentConfigDialog) {
+ ComponentConfigDialog.disposeDialog();
+ } else {
+ parent.dispose();
+ }
+ }
+ }
+
+ protected JPanel createCancelOperationContent() {
JPanel panel = new JPanel(new MigLayout());
String msg = isNewComponent ? trans.get("RocketCompCfg.CancelOperation.msg.undoAdd") :
trans.get("RocketCompCfg.CancelOperation.msg.discardChanges");
diff --git a/swing/src/net/sf/openrocket/gui/configdialog/SaveDesignInfoPanel.java b/swing/src/net/sf/openrocket/gui/configdialog/SaveDesignInfoPanel.java
new file mode 100644
index 000000000..ec53c60bf
--- /dev/null
+++ b/swing/src/net/sf/openrocket/gui/configdialog/SaveDesignInfoPanel.java
@@ -0,0 +1,104 @@
+package net.sf.openrocket.gui.configdialog;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.gui.components.StyledLabel;
+import net.sf.openrocket.gui.widgets.SelectColorButton;
+import net.sf.openrocket.l10n.Translator;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.startup.Preferences;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JDialog;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+/**
+ * This class is used to create a panel that is shown when a new design file is saved. It is used to fill in the design
+ * information for the file.
+ */
+public class SaveDesignInfoPanel extends RocketConfig {
+ private static final Translator trans = Application.getTranslator();
+ private static final Preferences preferences = Application.getPreferences();
+
+ public SaveDesignInfoPanel(OpenRocketDocument d, RocketComponent c, JDialog parent) {
+ super(d, c, parent);
+
+ // (Optional) Fill in the design information for this file
+ StyledLabel label = new StyledLabel(trans.get("SaveDesignInfoPanel.lbl.FillInInfo"), StyledLabel.Style.BOLD);
+ this.add(label, "spanx, wrap para", 0);
+ }
+
+ @Override
+ protected void addButtons(JButton... buttons) {
+ if (buttonPanel != null) {
+ this.remove(buttonPanel);
+ }
+
+ buttonPanel = new JPanel(new MigLayout("fill, ins 5, hidemode 3"));
+
+ //// Don't show this dialog again
+ JCheckBox dontShowAgain = new JCheckBox(trans.get("welcome.dlg.checkbox.dontShowAgain"));
+ dontShowAgain.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ preferences.setShowSaveRocketInfo(!((JCheckBox) e.getSource()).isSelected());
+ }
+ });
+ buttonPanel.add(dontShowAgain, "gapright 10, growx");
+
+ //// Cancel button
+ this.cancelButton = new SelectColorButton(trans.get("dlg.but.cancel"));
+ this.cancelButton.setToolTipText(trans.get("RocketCompCfg.btn.Cancel.ttip"));
+ cancelButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent arg0) {
+ // Don't do anything on cancel if you are editing an existing component, and it is not modified
+ if (!isNewComponent && parent != null && (parent instanceof ComponentConfigDialog && !((ComponentConfigDialog) parent).isModified())) {
+ disposeDialog();
+ return;
+ }
+ // Apply the cancel operation if set to auto discard in preferences
+ if (!preferences.isShowDiscardConfirmation()) {
+ ComponentConfigDialog.clearConfigListeners = false; // Undo action => config listeners of new component will be cleared
+ disposeDialog();
+ document.undo();
+ return;
+ }
+
+ // Yes/No dialog: Are you sure you want to discard your changes?
+ JPanel msg = createCancelOperationContent();
+ int resultYesNo = JOptionPane.showConfirmDialog(SaveDesignInfoPanel.this, msg,
+ trans.get("RocketCompCfg.CancelOperation.title"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
+ if (resultYesNo == JOptionPane.YES_OPTION) {
+ ComponentConfigDialog.clearConfigListeners = false; // Undo action => config listeners of new component will be cleared
+ disposeDialog();
+ document.undo();
+ }
+ }
+ });
+ buttonPanel.add(cancelButton, "split 2, right, gapleft 30lp");
+
+ //// Ok button
+ this.okButton = new SelectColorButton(trans.get("dlg.but.ok"));
+ this.okButton.setToolTipText(trans.get("RocketCompCfg.btn.OK.ttip"));
+ okButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent arg0) {
+ disposeDialog();
+ }
+ });
+ buttonPanel.add(okButton);
+
+ this.add(buttonPanel, "newline, spanx, growx");
+ }
+
+ @Override
+ public void updateFields() {
+ // Do nothing
+ }
+}
diff --git a/swing/src/net/sf/openrocket/gui/figureelements/RocketInfo.java b/swing/src/net/sf/openrocket/gui/figureelements/RocketInfo.java
index 92ba3ef88..1836ae644 100644
--- a/swing/src/net/sf/openrocket/gui/figureelements/RocketInfo.java
+++ b/swing/src/net/sf/openrocket/gui/figureelements/RocketInfo.java
@@ -226,25 +226,31 @@ public class RocketInfo implements FigureElement {
Rectangle2D stabTextRect = stabText.getVisualBounds();
Rectangle2D atTextRect = atText.getVisualBounds();
- double unitWidth = MathUtil.max(cpRect.getWidth(), cgRect.getWidth(), stabRect.getWidth());
+ double unitWidth = MathUtil.max(cpRect.getWidth(), cgRect.getWidth());
+ double stabUnitWidth = stabRect.getWidth();
double textWidth = Math.max(cpTextRect.getWidth(), cgTextRect.getWidth());
// Add an extra space worth of width so the text doesn't run into the values
unitWidth = unitWidth + spaceWidth;
+ stabUnitWidth = stabUnitWidth + spaceWidth;
g2.setColor(GUIUtil.getUITheme().getTextColor());
+ // Draw the stability, CG & CP values (and units)
g2.drawGlyphVector(stabValue, (float)(x2-stabRect.getWidth()), y1);
g2.drawGlyphVector(cgValue, (float)(x2-cgRect.getWidth()), y1+line);
g2.drawGlyphVector(cpValue, (float)(x2-cpRect.getWidth()), y1+2*line);
- g2.drawGlyphVector(stabText, (float)(x2-unitWidth-stabTextRect.getWidth()), y1);
+ // Draw the stability, CG & CP labels
+ g2.drawGlyphVector(stabText, (float)(x2-stabUnitWidth-stabTextRect.getWidth()), y1);
g2.drawGlyphVector(cgText, (float)(x2-unitWidth-cgTextRect.getWidth()), y1+line);
g2.drawGlyphVector(cpText, (float)(x2-unitWidth-cpTextRect.getWidth()), y1+2*line);
-
+
+ // Draw the CG caret
cgCaret.setPosition(x2 - unitWidth - textWidth - 10, y1+line-0.3*line);
cgCaret.paint(g2, 1.7);
+ // Draw the CP caret
cpCaret.setPosition(x2 - unitWidth - textWidth - 10, y1+2*line-0.3*line);
cpCaret.paint(g2, 1.7);
diff --git a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java
index d114b64f8..06e449c80 100644
--- a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java
+++ b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java
@@ -49,6 +49,7 @@ import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.configdialog.SaveDesignInfoPanel;
import net.sf.openrocket.gui.dialogs.ErrorWarningDialog;
import net.sf.openrocket.logging.ErrorSet;
import net.sf.openrocket.logging.WarningSet;
@@ -1596,6 +1597,9 @@ public class BasicFrame extends JFrame {
* @return true if the file was saved, false otherwise
*/
private boolean saveAsAction() {
+ // Open dialog for saving rocket info
+ showSaveRocketInfoDialog();
+
File file = openFileSaveAsDialog(FileType.OPENROCKET);
if (file == null) {
return false;
@@ -1610,6 +1614,25 @@ public class BasicFrame extends JFrame {
return result;
}
+ private void showSaveRocketInfoDialog() {
+ if (!prefs.isShowSaveRocketInfo()) {
+ return;
+ }
+
+ // Select the rocket in the component tree to indicate to users that they can edit the rocket info by editing the rocket
+ setSelectedComponent(rocket);
+
+ // Open the save rocket info
+ JDialog dialog = new JDialog();
+ SaveDesignInfoPanel panel = new SaveDesignInfoPanel(document, rocket, dialog);
+ dialog.setContentPane(panel);
+ dialog.pack();
+ dialog.setTitle(trans.get("BasicFrame.lbl.SaveRocketInfo"));
+ dialog.setModal(true);
+ dialog.setLocationRelativeTo(null);
+ dialog.setVisible(true);
+ }
+
/**
* Perform the writing of the design to the given file in OpenRocket format.
diff --git a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java
index c1de99517..4b5eb5e2f 100644
--- a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java
+++ b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java
@@ -111,6 +111,7 @@ public class SimulationPanel extends JPanel {
private final JPopupMenu pm;
private final SimulationAction editSimulationAction;
+ private final SimulationAction copyValuesSimulationAction;
private final SimulationAction runSimulationAction;
private final SimulationAction plotSimulationAction;
private final SimulationAction duplicateSimulationAction;
@@ -130,6 +131,7 @@ public class SimulationPanel extends JPanel {
// Simulation actions
SimulationAction newSimulationAction = new NewSimulationAction();
editSimulationAction = new EditSimulationAction();
+ copyValuesSimulationAction = new CopyValuesSimulationAction();
runSimulationAction = new RunSimulationAction();
plotSimulationAction = new PlotSimulationAction();
duplicateSimulationAction = new DuplicateSimulationAction();
@@ -156,7 +158,7 @@ public class SimulationPanel extends JPanel {
RocketActions.tieActionToButton(runButton, runSimulationAction, trans.get("simpanel.but.runsimulations"));
runButton.setToolTipText(trans.get("simpanel.but.ttip.runsimu"));
this.add(runButton, "gapright para");
-
+
//// Delete simulations button
deleteButton = new IconButton();
RocketActions.tieActionToButton(deleteButton, deleteSimulationAction, trans.get("simpanel.but.deletesimulations"));
@@ -185,10 +187,12 @@ public class SimulationPanel extends JPanel {
simulationTable.setDefaultRenderer(Object.class, new JLabelRenderer());
simulationTableModel.setColumnWidths(simulationTable.getColumnModel());
simulationTable.setFillsViewportHeight(true);
+ simulationTable.registerKeyboardAction(copyValuesSimulationAction, "Copy", RocketActions.COPY_KEY_STROKE, JComponent.WHEN_FOCUSED);
// Context menu
pm = new JPopupMenu();
pm.add(editSimulationAction);
+ pm.add(copyValuesSimulationAction);
pm.add(duplicateSimulationAction);
pm.add(deleteSimulationAction);
pm.addSeparator();
@@ -481,36 +485,40 @@ public class SimulationPanel extends JPanel {
}
- private void copySimulationAction() {
- int numCols=simulationTable.getColumnCount();
- int numRows=simulationTable.getSelectedRowCount();
- int[] rowsSelected=simulationTable.getSelectedRows();
-
- if (numRows!=rowsSelected[rowsSelected.length-1]-rowsSelected[0]+1 || numRows!=rowsSelected.length) {
+ private void copySimulationValuesAction() {
+ int numCols = simulationTable.getColumnCount();
+ int numRows = simulationTable.getSelectedRowCount();
+ int[] rowsSelected = simulationTable.getSelectedRows();
+ if (numRows != (rowsSelected[rowsSelected.length-1] - rowsSelected[0] + 1) || numRows != rowsSelected.length) {
JOptionPane.showMessageDialog(null, "Invalid Copy Selection", "Invalid Copy Selection", JOptionPane.ERROR_MESSAGE);
return;
}
- StringBuilder excelStr =new StringBuilder();
- for (int k = 1; k < numCols; k++) {
- excelStr.append(simulationTable.getColumnName(k));
- if (k < numCols-1) {
- excelStr.append("\t");
+ StringBuilder valuesStr = new StringBuilder();
+
+ // Copy the column names
+ valuesStr.append(trans.get("simpanel.col.Status")).append("\t");
+ for (int i = 1; i < numCols; i++) {
+ valuesStr.append(simulationTable.getColumnName(i));
+ if (i < numCols-1) {
+ valuesStr.append("\t");
}
}
- excelStr.append("\n");
+ valuesStr.append("\n");
+
+ // Copy the values
for (int i = 0; i < numRows; i++) {
- for (int j = 1; j < numCols; j++) {
- excelStr.append(simulationTable.getValueAt(rowsSelected[i], j));
+ for (int j = 0; j < numCols; j++) {
+ valuesStr.append(simulationTable.getValueAt(rowsSelected[i], j).toString());
if (j < numCols-1) {
- excelStr.append("\t");
+ valuesStr.append("\t");
}
}
- excelStr.append("\n");
+ valuesStr.append("\n");
}
- StringSelection sel = new StringSelection(excelStr.toString());
+ StringSelection sel = new StringSelection(valuesStr.toString());
Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
cb.setContents(sel, sel);
@@ -546,6 +554,7 @@ public class SimulationPanel extends JPanel {
private void updateButtonStates() {
editSimulationAction.updateEnabledState();
+ copyValuesSimulationAction.updateEnabledState();
deleteSimulationAction.updateEnabledState();
runSimulationAction.updateEnabledState();
plotSimulationAction.updateEnabledState();
@@ -596,6 +605,61 @@ public class SimulationPanel extends JPanel {
return simulationTable.getSelectionModel();
}
+ private String getSimulationToolTip(Simulation sim, boolean includeSimName) {
+ String tip;
+ FlightData data = sim.getSimulatedData();
+
+ tip = "";
+ if (includeSimName) {
+ tip += "" + sim.getName() + "
";
+ }
+ switch (sim.getStatus()) {
+ case CANT_RUN:
+ tip += trans.get("simpanel.ttip.noData")+"
";
+ break;
+ case LOADED:
+ tip += trans.get("simpanel.ttip.loaded") + "
";
+ break;
+ case UPTODATE:
+ tip += trans.get("simpanel.ttip.uptodate") + "
";
+ break;
+
+ case OUTDATED:
+ tip += trans.get("simpanel.ttip.outdated") + "
";
+ break;
+
+ case EXTERNAL:
+ tip += trans.get("simpanel.ttip.external") + "
";
+ return tip;
+
+ case NOT_SIMULATED:
+ tip += trans.get("simpanel.ttip.notSimulated");
+ return tip;
+ }
+
+ if (data == null) {
+ tip += trans.get("simpanel.ttip.noData");
+ return tip;
+ }
+ WarningSet warnings = data.getWarningSet();
+
+ if (warnings.isEmpty()) {
+ tip += trans.get("simpanel.ttip.noWarnings");
+ return tip;
+ }
+
+ tip += trans.get("simpanel.ttip.warnings");
+ for (Warning w : warnings) {
+ tip += "
" + w.toString();
+ }
+
+ return tip;
+ }
+
+ private String getSimulationToolTip(Simulation sim) {
+ return getSimulationToolTip(sim, true);
+ }
+
private void openDialog(boolean plotMode, boolean isNewSimulation, final Simulation... sims) {
SimulationEditDialog d = new SimulationEditDialog(SwingUtilities.getWindowAncestor(this), document, isNewSimulation, sims);
if (plotMode) {
@@ -671,6 +735,25 @@ public class SimulationPanel extends JPanel {
}
}
+ class CopyValuesSimulationAction extends SimulationAction {
+ public CopyValuesSimulationAction() {
+ putValue(NAME, trans.get("simpanel.pop.copyValues"));
+ this.putValue(MNEMONIC_KEY, KeyEvent.VK_C);
+ this.putValue(ACCELERATOR_KEY, RocketActions.COPY_KEY_STROKE);
+ this.putValue(SMALL_ICON, Icons.EDIT_COPY);
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ copySimulationValuesAction();
+ }
+
+ @Override
+ public void updateEnabledState() {
+ setEnabled(simulationTable.getSelectedRowCount() > 0);
+ }
+ }
+
class RunSimulationAction extends SimulationAction {
public RunSimulationAction() {
putValue(NAME, trans.get("simpanel.pop.run"));
@@ -853,53 +936,24 @@ public class SimulationPanel extends JPanel {
}
return component;
}
+ }
- private String getSimulationToolTip(Simulation sim) {
- String tip;
- FlightData data = sim.getSimulatedData();
+ private class StatusLabel extends StyledLabel {
+ private Simulation simulation;
- tip = "" + sim.getName() + "
";
- switch (sim.getStatus()) {
- case CANT_RUN:
- tip += trans.get("simpanel.ttip.noData")+"
";
- break;
- case LOADED:
- tip += trans.get("simpanel.ttip.loaded") + "
";
- break;
- case UPTODATE:
- tip += trans.get("simpanel.ttip.uptodate") + "
";
- break;
+ public StatusLabel(Simulation simulation, float size) {
+ super(size);
+ this.simulation = simulation;
+ }
- case OUTDATED:
- tip += trans.get("simpanel.ttip.outdated") + "
";
- break;
+ public void replaceSimulation(Simulation simulation) {
+ this.simulation = simulation;
+ }
- case EXTERNAL:
- tip += trans.get("simpanel.ttip.external") + "
";
- return tip;
-
- case NOT_SIMULATED:
- tip += trans.get("simpanel.ttip.notSimulated");
- return tip;
- }
-
- if (data == null) {
- tip += trans.get("simpanel.ttip.noData");
- return tip;
- }
- WarningSet warnings = data.getWarningSet();
-
- if (warnings.isEmpty()) {
- tip += trans.get("simpanel.ttip.noWarnings");
- return tip;
- }
-
- tip += trans.get("simpanel.ttip.warnings");
- for (Warning w : warnings) {
- tip += "
" + w.toString();
- }
-
- return tip;
+ @Override
+ public String toString() {
+ String text = getSimulationToolTip(simulation, false);
+ return text.replace("
", "-").replaceAll("<[^>]*>","");
}
}
@@ -910,31 +964,33 @@ public class SimulationPanel extends JPanel {
super(
//// Status and warning column
new Column("") {
- private JLabel label = null;
+ private StatusLabel label = null;
@Override
public Object getValueAt(int row) {
if (row < 0 || row >= document.getSimulationCount())
return null;
+ Simulation simulation = document.getSimulation(row);
+
// Initialize the label
if (label == null) {
- label = new StyledLabel(2f);
+ label = new StatusLabel(simulation, 2f);
label.setIconTextGap(1);
// label.setFont(label.getFont().deriveFont(Font.BOLD));
+ } else {
+ label.replaceSimulation(simulation);
}
// Set simulation status icon
- Simulation.Status status = document.getSimulation(row).getStatus();
+ Simulation.Status status = simulation.getStatus();
label.setIcon(Icons.SIMULATION_STATUS_ICON_MAP.get(status));
// Set warning marker
if (status == Simulation.Status.NOT_SIMULATED ||
status == Simulation.Status.EXTERNAL) {
-
label.setText("");
-
} else {
WarningSet w = document.getSimulation(row).getSimulatedWarnings();
diff --git a/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/SeparationConfigurationPanel.java b/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/SeparationConfigurationPanel.java
index 306877c46..c978649ce 100644
--- a/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/SeparationConfigurationPanel.java
+++ b/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/SeparationConfigurationPanel.java
@@ -181,10 +181,13 @@ public class SeparationConfigurationPanel extends FlightConfigurablePanel 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);