diff --git a/core/src/net/sf/openrocket/document/OpenRocketDocument.java b/core/src/net/sf/openrocket/document/OpenRocketDocument.java index eb0f26d7a..41c32acac 100644 --- a/core/src/net/sf/openrocket/document/OpenRocketDocument.java +++ b/core/src/net/sf/openrocket/document/OpenRocketDocument.java @@ -810,7 +810,7 @@ public class OpenRocketDocument implements ComponentChangeListener { listeners.remove(listener); } - protected void fireDocumentChangeEvent(DocumentChangeEvent event) { + public void fireDocumentChangeEvent(DocumentChangeEvent event) { DocumentChangeListener[] array = listeners.toArray(new DocumentChangeListener[0]); for (DocumentChangeListener l : array) { l.documentChanged(event); diff --git a/core/src/net/sf/openrocket/models/atmosphere/InterpolatingAtmosphericModel.java b/core/src/net/sf/openrocket/models/atmosphere/InterpolatingAtmosphericModel.java index 6b383b513..780adc300 100644 --- a/core/src/net/sf/openrocket/models/atmosphere/InterpolatingAtmosphericModel.java +++ b/core/src/net/sf/openrocket/models/atmosphere/InterpolatingAtmosphericModel.java @@ -18,10 +18,20 @@ public abstract class InterpolatingAtmosphericModel implements AtmosphericModel if (levels == null) computeLayers(); - if (altitude <= 0) + if (altitude <= 0) { + // TODO: LOW: levels[0] returned null in some cases, see GitHub issue #952 for more information + if (levels[0] == null) { + computeLayers(); + } return levels[0]; - if (altitude >= DELTA * (levels.length - 1)) + } + if (altitude >= DELTA * (levels.length - 1)) { + // TODO: LOW: levels[levels.length - 1] returned null in some cases, see GitHub issue #952 for more information + if (levels[levels.length - 1] == null) { + computeLayers(); + } return levels[levels.length - 1]; + } int n = (int) (altitude / DELTA); double d = (altitude - n * DELTA) / DELTA; diff --git a/core/src/net/sf/openrocket/motor/MotorConfiguration.java b/core/src/net/sf/openrocket/motor/MotorConfiguration.java index 4920a9421..48a42687c 100644 --- a/core/src/net/sf/openrocket/motor/MotorConfiguration.java +++ b/core/src/net/sf/openrocket/motor/MotorConfiguration.java @@ -115,6 +115,8 @@ public class MotorConfiguration implements FlightConfigurableParameter { @@ -154,5 +156,17 @@ public class DeploymentConfiguration implements FlightConfigurableParameter { public static enum SeparationEvent { @@ -144,8 +146,20 @@ public class StageSeparationConfiguration implements FlightConfigurableParameter clone.separationDelay = this.separationDelay; return clone; } - - + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StageSeparationConfiguration that = (StageSeparationConfiguration) o; + return Double.compare(that.separationDelay, separationDelay) == 0 && separationEvent == that.separationEvent; + } + + @Override + public int hashCode() { + return Objects.hash(separationEvent, separationDelay); + } + private void fireChangeEvent() { } diff --git a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java index 82cccd9ed..82723a6e1 100644 --- a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java +++ b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java @@ -259,7 +259,7 @@ public class BasicEventSimulationEngine implements SimulationEngine { private boolean handleEvents() throws SimulationException { boolean ret = true; FlightEvent event; - + log.trace("HandleEvents: current branch = " + currentStatus.getFlightData().getBranchName()); for (event = nextEvent(); event != null; event = nextEvent()) { log.trace("Obtained event from queue: " + event.toString()); @@ -300,7 +300,7 @@ public class BasicEventSimulationEngine implements SimulationEngine { // Ignore events for components that are no longer attached to the rocket if (event.getSource() != null && event.getSource().getParent() != null && !currentStatus.getConfiguration().isComponentActive(event.getSource())) { - log.trace("Ignoring event from unattached componenent"); + log.trace("Ignoring event from unattached component"); continue; } @@ -332,6 +332,7 @@ public class BasicEventSimulationEngine implements SimulationEngine { // Check for recovery device deployment, add events to queue + // TODO: LOW: check if deprecated function getActiveComponents needs to be replaced for (RocketComponent c : currentStatus.getConfiguration().getActiveComponents()) { if (!(c instanceof RecoveryDevice)) continue; @@ -532,7 +533,8 @@ public class BasicEventSimulationEngine implements SimulationEngine { } } - + + // TODO : FUTURE : do not hard code the 1200 (maybe even make it configurable by the user) if( 1200 < currentStatus.getSimulationTime() ){ ret = false; log.error("Simulation hit max time (1200s): aborting."); diff --git a/core/src/net/sf/openrocket/simulation/FlightData.java b/core/src/net/sf/openrocket/simulation/FlightData.java index e7f24f806..6cdb356e1 100644 --- a/core/src/net/sf/openrocket/simulation/FlightData.java +++ b/core/src/net/sf/openrocket/simulation/FlightData.java @@ -220,7 +220,7 @@ public class FlightData { timeToApogee = Double.NaN; - // Launch rod velocity + // Launch rod velocity + deployment velocity + ground hit velocity for (FlightEvent event : branch.getEvents()) { if (event.getType() == FlightEvent.Type.LAUNCHROD) { double t = event.getTime(); diff --git a/core/src/net/sf/openrocket/simulation/listeners/system/GroundHitListener.java b/core/src/net/sf/openrocket/simulation/listeners/system/GroundHitListener.java new file mode 100644 index 000000000..96d9ffd1b --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/listeners/system/GroundHitListener.java @@ -0,0 +1,29 @@ +package net.sf.openrocket.simulation.listeners.system; + +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; + + +/** + * A simulation listeners that ends the simulation when the ground is hit. + * + * @author Sibo Van Gool + */ +public class GroundHitListener extends AbstractSimulationListener { + + public static final GroundHitListener INSTANCE = new GroundHitListener(); + + @Override + public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) { + if (event.getType() == FlightEvent.Type.GROUND_HIT) { + status.getEventQueue().add(new FlightEvent(FlightEvent.Type.SIMULATION_END, status.getSimulationTime())); + } + return true; + } + + @Override + public boolean isSystemListener() { + return true; + } +} diff --git a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java index 1818a533e..a5a6f27b9 100644 --- a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java +++ b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java @@ -221,7 +221,7 @@ public class BasicFrame extends JFrame { // Bottom segment, rocket figure - rocketpanel = new RocketPanel(document); + rocketpanel = new RocketPanel(document, this); vertical.setBottomComponent(rocketpanel); rocketpanel.setSelectionModel(tree.getSelectionModel()); @@ -1099,6 +1099,9 @@ public class BasicFrame extends JFrame { tabbedPane.setSelectedIndex(tab); } + public int getSelectedTab() { + return tabbedPane.getSelectedIndex(); + } private void openAction() { diff --git a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java index 2488234e7..7657bba57 100644 --- a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java +++ b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java @@ -576,7 +576,7 @@ public class SimulationPanel extends JPanel { public void documentChanged(DocumentChangeEvent event) { if (!(event instanceof SimulationChangeEvent)) return; - simulationTableModel.fireTableDataChanged(); + fireMaintainSelection(); } }); diff --git a/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/FlightConfigurablePanel.java b/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/FlightConfigurablePanel.java index 39490f44e..54071c1f1 100644 --- a/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/FlightConfigurablePanel.java +++ b/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/FlightConfigurablePanel.java @@ -54,10 +54,14 @@ public abstract class FlightConfigurablePanel throw new IllegalStateException("Attempting to set a motor on the default FCID."); } + double initDelay = curMount.getMotorConfig(fcid).getEjectionDelay(); + motorChooserDialog.setMotorMountAndConfig( fcid, curMount ); motorChooserDialog.setVisible(true); Motor mtr = motorChooserDialog.getSelectedMotor(); double d = motorChooserDialog.getSelectedDelay(); if (mtr != null) { + if (mtr == curMount.getMotorConfig(fcid).getMotor() && d == initDelay) { + return; + } final MotorConfiguration templateConfig = curMount.getMotorConfig(fcid); final MotorConfiguration newConfig = new MotorConfiguration( curMount, fcid, templateConfig); newConfig.setMotor(mtr); newConfig.setEjectionDelay(d); curMount.setMotorConfig( newConfig, fcid); - } - fireTableDataChanged(); + fireTableDataChanged(ComponentChangeEvent.MOTOR_CHANGE); + } } private void removeMotor() { @@ -226,24 +233,30 @@ public class MotorConfigurationPanel extends FlightConfigurablePanel curMount.setMotorConfig( null, fcid); - fireTableDataChanged(); + fireTableDataChanged(ComponentChangeEvent.MOTOR_CHANGE); } private void selectIgnition() { - MotorMount curMount = getSelectedComponent(); - FlightConfigurationId fcid= getSelectedConfigurationId(); - if ( (null == fcid )||( null == curMount )){ - return; - } - + MotorMount curMount = getSelectedComponent(); + FlightConfigurationId fcid = getSelectedConfigurationId(); + if ((null == fcid) || (null == curMount)) { + return; + } + + MotorConfiguration curInstance = curMount.getMotorConfig(fcid); + IgnitionEvent initialIgnitionEvent = curInstance.getIgnitionEvent(); + double initialIgnitionDelay = curInstance.getIgnitionDelay(); + // this call also performs the update changes IgnitionSelectionDialog ignitionDialog = new IgnitionSelectionDialog( SwingUtilities.getWindowAncestor(this.flightConfigurationPanel), fcid, curMount); ignitionDialog.setVisible(true); - - fireTableDataChanged(); + + if (!initialIgnitionEvent.equals(curInstance.getIgnitionEvent()) || (initialIgnitionDelay != curInstance.getIgnitionDelay())) { + fireTableDataChanged(ComponentChangeEvent.MOTOR_CHANGE); + } } @@ -254,10 +267,14 @@ public class MotorConfigurationPanel extends FlightConfigurablePanel return; } MotorConfiguration curInstance = curMount.getMotorConfig(fcid); + IgnitionEvent initialIgnitionEvent = curInstance.getIgnitionEvent(); + double initialIgnitionDelay = curInstance.getIgnitionDelay(); curInstance.useDefaultIgnition(); - fireTableDataChanged(); + if (!initialIgnitionEvent.equals(curInstance.getIgnitionEvent()) || (initialIgnitionDelay != curInstance.getIgnitionDelay())) { + fireTableDataChanged(ComponentChangeEvent.MOTOR_CHANGE); + } } diff --git a/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/RecoveryConfigurationPanel.java b/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/RecoveryConfigurationPanel.java index 80c71a8fd..9fd246ec2 100644 --- a/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/RecoveryConfigurationPanel.java +++ b/swing/src/net/sf/openrocket/gui/main/flightconfigpanel/RecoveryConfigurationPanel.java @@ -99,22 +99,31 @@ public class RecoveryConfigurationPanel extends FlightConfigurablePanel * @author Bill Kuker */ @@ -87,6 +86,7 @@ import net.sf.openrocket.util.StateChangeListener; public class RocketPanel extends JPanel implements TreeSelectionListener, ChangeSource { private static final Translator trans = Application.getTranslator(); + private static final Logger log = LoggerFactory.getLogger(RocketPanel.class); public enum VIEW_TYPE { SideView(false, RocketFigure.VIEW_SIDE), @@ -128,7 +128,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change /* Calculation of CP and CG */ private AerodynamicCalculator aerodynamicCalculator; - + private final OpenRocketDocument document; private Caret extraCP = null; @@ -148,13 +148,16 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change private List listeners = new ArrayList(); + // Store the basic frame to know which tab is selected (Rocket design, Motors & Configuration, Flight simulations) + private final BasicFrame basicFrame; + /** * The executor service used for running the background simulations. * This uses a fixed-sized thread pool for all background simulations * with all threads in daemon mode and with minimum priority. */ - private static final Executor backgroundSimulationExecutor; + private static final ExecutorService backgroundSimulationExecutor; static { backgroundSimulationExecutor = Executors.newFixedThreadPool(SwingPreferences.getMaxThreadCount(), new ThreadFactory() { @@ -170,13 +173,17 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change }); } - public OpenRocketDocument getDocument(){ return this.document; } - + public RocketPanel(OpenRocketDocument document) { + this(document, null); + } + + public RocketPanel(OpenRocketDocument document, BasicFrame basicFrame) { this.document = document; + this.basicFrame = basicFrame; Rocket rkt = document.getRocket(); @@ -216,6 +223,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change rkt.addComponentChangeListener(new ComponentChangeListener() { @Override public void componentChanged(ComponentChangeEvent e) { + updateExtras(); if (is3d) { if (e.isTextureChange()) { figure3d.flushTextureCaches(); @@ -367,7 +375,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change /** * Get the center of pressure figure element. - * + * * @return center of pressure info */ public Caret getExtraCP() { @@ -376,7 +384,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change /** * Get the center of gravity figure element. - * + * * @return center of gravity info */ public Caret getExtraCG() { @@ -385,7 +393,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change /** * Get the extra text figure element. - * + * * @return extra text that contains info about the rocket design */ public RocketInfo getExtraText() { @@ -490,12 +498,12 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change /** * Handle clicking on figure shapes. The functioning is the following: - * + * * Get the components clicked. * If no component is clicked, do nothing. - * If the currently selected component is in the set, keep it, - * unless the selector specified is pressed. If it is pressed, cycle to - * the next component. Otherwise select the first component in the list. + * If the currently selected component is in the set, keep it, + * unless the selector specified is pressed. If it is pressed, cycle to + * the next component. Otherwise select the first component in the list. */ public static final int CYCLE_SELECTION_MODIFIER = InputEvent.SHIFT_DOWN_MASK; @@ -557,7 +565,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change /** * Updates the extra data included in the figure. Currently this includes - * the CP and CG carets. + * the CP and CG carets. Also start the background simulator. */ private WarningSet warnings = new WarningSet(); @@ -645,7 +653,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change figure3d.setCG(new Coordinate(Double.NaN, Double.NaN)); figure3d.setCP(new Coordinate(Double.NaN, Double.NaN)); } - + if (figure.getType() == RocketPanel.VIEW_TYPE.SideView && length > 0) { extraCP.setPosition(cpx, cpy); extraCG.setPosition(cgx, cgy); @@ -683,35 +691,120 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change return; } - // Start calculation process - if(((SwingPreferences) Application.getPreferences()).computeFlightInBackground()){ - extraText.setCalculatingData(true); + // Update simulations + if (Application.getPreferences().getAutoRunSimulations()) { + // Update only current flight config simulation when you are not in the simulations tab + updateSims(this.basicFrame != null && this.basicFrame.getSelectedTab() == BasicFrame.SIMULATION_TAB); + } + else { + // Always update the simulation of the current configuration + updateSims(false); + } - Rocket duplicate = (Rocket) document.getRocket().copy(); + // Update flight data and add flight data update trigger upon simulation changes + for (Simulation sim : document.getSimulations()) { + sim.addChangeListener(new StateChangeListener() { + @Override + public void stateChanged(EventObject e) { + if (updateFlightData(sim)) { + updateFigures(); + } + } + }); + if (updateFlightData(sim)) { + break; + } + } + } - // find a Simulation based on the current flight configuration - FlightConfigurationId curID = curConfig.getFlightConfigurationID(); - Simulation simulation = null; - for (Simulation sim : document.getSimulations()) { + /** + * Updates the simulations. If *currentConfig* is false, only update the simulation of the current flight + * configuration. If it is true, update all the simulations. + * + * @param updateAllSims flag to check whether to update all the simulations (true) or only the current + * flight config sim (false) + */ + private void updateSims(boolean updateAllSims) { + // Stop previous computation (if any) + stopBackgroundSimulation(); + + FlightConfigurationId curID = document.getSelectedConfiguration().getFlightConfigurationID(); + extraText.setCalculatingData(true); + Rocket duplicate = (Rocket)document.getRocket().copy(); + + // Re-run the present simulation(s) + List sims = new LinkedList<>(); + for (Simulation sim : document.getSimulations()) { + if (sim.getStatus() == Simulation.Status.UPTODATE || sim.getStatus() == Simulation.Status.LOADED + || !document.getRocket().getFlightConfiguration(sim.getFlightConfigurationId()).hasMotors()) + continue; + + // Find a Simulation based on the current flight configuration + if (!updateAllSims) { if (sim.getFlightConfigurationId().compareTo(curID) == 0) { - simulation = sim; + sims.add(sim); break; } } - - // I *think* every FlightConfiguration has at least one associated simulation; just in case I'm wrong, - // if there isn't one we'll create a new simulation to update the statistics in the panel using the - // default simulation conditions - if (simulation == null) { - System.out.println("creating new simulation"); - simulation = ((SwingPreferences) Application.getPreferences()).getBackgroundSimulation(duplicate); - simulation.setFlightConfigurationId( document.getSelectedConfiguration().getId()); - } else - System.out.println("using pre-existing simulation"); - - backgroundSimulationWorker = new BackgroundSimulationWorker(document, simulation); - backgroundSimulationExecutor.execute(backgroundSimulationWorker); + else { + sims.add(sim); + } } + runBackgroundSimulations(sims, duplicate); + } + + /** + * Update the flight data text with the data of {sim}. Only update if sim is the simulation of the current flight + * configuration. + * @param sim: simulation from which the flight data is taken + * @return true if the flight data was updated, false if not + */ + private boolean updateFlightData(Simulation sim) { + FlightConfigurationId curID = document.getSelectedConfiguration().getFlightConfigurationID(); + if (sim.getFlightConfigurationId().compareTo(curID) == 0) { + if (sim.hasSimulationData()) { + extraText.setFlightData(sim.getSimulatedData()); + } else { + extraText.setFlightData(FlightData.NaN_DATA); + } + return true; + } + return false; + } + + /** + * Runs a new background simulation for simulations *sims*. It will run all the simulations in sims sequentially + * in the background. + * + * @param sims simulations which should be run + * @param rkt rocket for which the simulations are run + */ + private void runBackgroundSimulations(List sims, Rocket rkt) { + if (sims.size() == 0) { + extraText.setCalculatingData(false); + for (Simulation sim : document.getSimulations()) { + if (updateFlightData(sim)) { + return; + } + } + extraText.setFlightData(FlightData.NaN_DATA); + return; + } + + // I *think* every FlightConfiguration has at least one associated simulation; just in case I'm wrong, + // if there isn't one we'll create a new simulation to update the statistics in the panel using the + // default simulation conditions + for (Simulation sim : sims) { + if (sim == null) { + log.info("creating new simulation"); + sim = ((SwingPreferences) Application.getPreferences()).getBackgroundSimulation(rkt); + sim.setFlightConfigurationId(document.getSelectedConfiguration().getId()); + } else + log.info("using pre-existing simulation"); + } + + backgroundSimulationWorker = new BackgroundSimulationWorker(document, sims); + backgroundSimulationExecutor.execute(backgroundSimulationWorker); } /** @@ -732,16 +825,20 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change private class BackgroundSimulationWorker extends SimulationWorker { private final CustomExpressionSimulationListener exprListener; + private final OpenRocketDocument doc; + private List sims; - public BackgroundSimulationWorker(OpenRocketDocument doc, Simulation sim) { - super(sim); + public BackgroundSimulationWorker(OpenRocketDocument doc, List sims) { + super(sims.get(0)); + this.sims = sims; + this.doc = doc; List exprs = doc.getCustomExpressions(); exprListener = new CustomExpressionSimulationListener(exprs); } @Override protected FlightData doInBackground() { - + extraText.setCalculatingData(true); // Pause a little while to allow faster UI reaction try { Thread.sleep(300); @@ -749,7 +846,6 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change } if (isCancelled() || backgroundSimulationWorker != this) return null; - return super.doInBackground(); } @@ -758,19 +854,27 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change // Do nothing if cancelled if (isCancelled() || backgroundSimulationWorker != this) return; - backgroundSimulationWorker = null; - extraText.setFlightData(simulation.getSimulatedData()); + + // Only set the flight data information of the current flight configuration extraText.setCalculatingData(false); figure.repaint(); figure3d.repaint(); + document.fireDocumentChangeEvent(new SimulationChangeEvent(simulation)); + + // Run the new simulation after this one has ended + this.sims.remove(0); + if (this.sims.size() > 0) { + backgroundSimulationWorker = new BackgroundSimulationWorker(this.doc, this.sims); + backgroundSimulationExecutor.execute(backgroundSimulationWorker); + } } @Override protected SimulationListener[] getExtraListeners() { return new SimulationListener[] { InterruptListener.INSTANCE, - ApogeeEndListener.INSTANCE, + GroundHitListener.INSTANCE, exprListener }; } @@ -813,7 +917,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change } /** - * Updates the selection in the FigureParameters and repaints the figure. + * Updates the selection in the FigureParameters and repaints the figure. * Ignores the event itself. */ @Override @@ -868,5 +972,5 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change // putValue(Action.SELECTED_KEY, figure.getType() == type && !is3d); // } // } - + }