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