From b673a591f2b5fc0ec08fb8daa4393cf6f2457a9f Mon Sep 17 00:00:00 2001 From: Billy Olsen Date: Sun, 10 May 2020 21:10:24 -0700 Subject: [PATCH] Update print dialog to allow simulation control Updates the print dialog to allow for simulations to be re-run or not. Selecting the "Update simulation data" checkbox will re-run any out of date simulations prior to generating the design report. Fixes #637 Signed-off-by: Billy Olsen --- core/resources/l10n/messages.properties | 1 + .../openrocket/gui/dialogs/PrintDialog.java | 32 ++- .../sf/openrocket/gui/print/DesignReport.java | 226 ++++++++++++++++-- .../openrocket/gui/print/PrintController.java | 40 +++- 4 files changed, 269 insertions(+), 30 deletions(-) diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index 3226bb598..919292a94 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -115,6 +115,7 @@ AboutDialog.lbl.translatorIcon = PrintDialog.title = Print or export PrintDialog.but.previewAndPrint = Preview & Print PrintDialog.checkbox.showByStage = Show by stage +PrintDialog.checkbox.updateSimulations = Update simulation data PrintDialog.lbl.selectElements = Select elements to include: printdlg.but.saveaspdf = Save as PDF printdlg.but.preview = Preview diff --git a/swing/src/net/sf/openrocket/gui/dialogs/PrintDialog.java b/swing/src/net/sf/openrocket/gui/dialogs/PrintDialog.java index 750052835..d944d1d00 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/PrintDialog.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/PrintDialog.java @@ -50,6 +50,8 @@ import net.sf.openrocket.startup.Application; */ public class PrintDialog extends JDialog implements TreeSelectionListener { + private static final long serialVersionUID = 1L; + private static final Logger log = LoggerFactory.getLogger(PrintDialog.class); private static final Translator trans = Application.getTranslator(); @@ -65,6 +67,8 @@ public class PrintDialog extends JDialog implements TreeSelectionListener { private JButton cancel; private double rotation = 0d; + + private boolean updateSimulations = true; private final static SwingPreferences prefs = (SwingPreferences) Application.getPreferences(); @@ -122,6 +126,19 @@ public class PrintDialog extends JDialog implements TreeSelectionListener { // Checkboxes and buttons + final JPanel optionsPanel = new JPanel(new MigLayout()); + + final JCheckBox updateSimulationsCheckbox = new JCheckBox(trans.get("checkbox.updateSimulations")); + updateSimulationsCheckbox.setEnabled(true); + updateSimulationsCheckbox.setSelected(this.updateSimulations); + updateSimulationsCheckbox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + updateSimulations = updateSimulationsCheckbox.isSelected(); + } + }); + optionsPanel.add(updateSimulationsCheckbox, "pad 0, grow, wrap"); + final JCheckBox sortByStage = new JCheckBox(trans.get("checkbox.showByStage")); sortByStage.setEnabled(stages > 1); sortByStage.setSelected(stages > 1); @@ -142,10 +159,10 @@ public class PrintDialog extends JDialog implements TreeSelectionListener { } } }); - panel.add(sortByStage, "aligny top, split"); + optionsPanel.add(sortByStage); + panel.add(optionsPanel, "pad 0, aligny top, split"); - - panel.add(new JPanel(), "growx"); + panel.add(new JPanel(), "pad 0, aligny top, growx"); JButton settingsButton = new JButton(trans.get("printdlg.but.settings")); @@ -159,8 +176,8 @@ public class PrintDialog extends JDialog implements TreeSelectionListener { setPrintSettings(settings); } }); - panel.add(settingsButton, "wrap para"); - + panel.add(settingsButton, "aligny top, wrap para"); + previewButton = new JButton(trans.get("but.previewAndPrint")); previewButton.addActionListener(new ActionListener() { @@ -286,7 +303,10 @@ public class PrintDialog extends JDialog implements TreeSelectionListener { */ private File generateReport(File f, PrintSettings settings) throws IOException { Iterator toBePrinted = currentTree.getToBePrinted(); - new PrintController().print(document, toBePrinted, new FileOutputStream(f), settings, rotation); + PrintController controller = new PrintController(); + controller.setWindow(this.getOwner()); + controller.print(document, toBePrinted, new FileOutputStream(f), + settings, rotation, updateSimulations); return f; } diff --git a/swing/src/net/sf/openrocket/gui/print/DesignReport.java b/swing/src/net/sf/openrocket/gui/print/DesignReport.java index dda55bd73..84f9a854f 100644 --- a/swing/src/net/sf/openrocket/gui/print/DesignReport.java +++ b/swing/src/net/sf/openrocket/gui/print/DesignReport.java @@ -4,8 +4,15 @@ package net.sf.openrocket.gui.print; import java.awt.Graphics2D; +import java.awt.Window; import java.text.DecimalFormat; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,6 +34,7 @@ import net.sf.openrocket.formatting.RocketDescriptor; import net.sf.openrocket.gui.figureelements.FigureElement; import net.sf.openrocket.gui.figureelements.RocketInfo; import net.sf.openrocket.gui.scalefigure.RocketPanel; +import net.sf.openrocket.gui.simulation.SimulationRunDialog; import net.sf.openrocket.masscalc.MassCalculator; import net.sf.openrocket.masscalc.RigidBody; import net.sf.openrocket.motor.Motor; @@ -38,6 +46,7 @@ import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.FlightDataBranch; import net.sf.openrocket.simulation.exception.SimulationException; import net.sf.openrocket.startup.Application; import net.sf.openrocket.unit.Unit; @@ -107,6 +116,21 @@ public class DesignReport { */ private double rotation = 0d; + /** + * Determines whether or not to run out of date simulations. + */ + private boolean runOutOfDateSimulations = true; + + /** + * Determines whether or not to update existing simulations. + */ + private boolean updateExistingSimulations = false; + + /** + * Parent window for showing simulation run dialog as necessary + */ + private Window window = null; + /** The displayed strings. */ private static final String STAGES = "Stages: "; private static final String MASS_WITH_MOTORS = "Mass (with motors): "; @@ -135,17 +159,45 @@ public class DesignReport { private static final double GRAVITY_CONSTANT = 9.80665d; /** - * Constructor. + * Creates a new DesignReport in the iTextPDF Document based on the + * OpenRocketDocument specified. All out of date simulations will be + * run as part of generating the iTextPDF report. + * + * This is for backwards API compatibility and will copy existing + * simulations before running them. + * + * @param theRocDoc the OpenRocketDocument which serves as the source + * of the rocket information + * @param theIDoc the iTextPDF Document where the DesignReport is written + * @param figureRotation the rotation of the figure used for displaying + * the profile view. + */ + public DesignReport(OpenRocketDocument theRocDoc, Document theIDoc, Double figureRotation) { + this(theRocDoc, theIDoc, figureRotation, true, false, null); + } + + /** + * Creates a new DesignReport in the iTextPDF Document based on the + * OpenRocketDocument specified. Out of date simulations will be run + * when the runOutOfDateSims parameter is set to true. * * @param theRocDoc the OR document * @param theIDoc the iText document * @param figureRotation the angle the figure is rotated on the screen; printed report will mimic + * @param runOutOfDateSims whether or not to run simulations that are not up to date. + * @param updateExistingSims whether or not to update existing simulations or to copy the simulations. + * Previous behavior was to copy existing simulations. + * @param window the base AWT window to use */ - public DesignReport(OpenRocketDocument theRocDoc, Document theIDoc, Double figureRotation) { + public DesignReport(OpenRocketDocument theRocDoc, Document theIDoc, Double figureRotation, + boolean runOutOfDateSims, boolean updateExistingSims, Window window) { document = theIDoc; rocketDocument = theRocDoc; panel = new RocketPanel(rocketDocument); rotation = figureRotation; + this.runOutOfDateSimulations = runOutOfDateSims; + this.updateExistingSimulations = updateExistingSims; + this.window = window; } /** @@ -226,7 +278,7 @@ public class DesignReport { paragraph.setSpacingAfter(heightOfDiagramAndText); document.add(paragraph); - List simulations = rocketDocument.getSimulations(); + List simulations = getSimulations(); boolean firstMotor = true; for (FlightConfigurationId fcid : rocket.getIds()) { @@ -242,7 +294,7 @@ public class DesignReport { */ int leading = (firstMotor) ? 0 : 25; - FlightData flight = findSimulation( fcid, simulations); + FlightData flight = findSimulation(fcid, simulations); addFlightData(flight, rocket, fcid, parent, leading); addMotorData(rocket, fcid, parent); document.add(parent); @@ -456,6 +508,10 @@ public class DesignReport { // Output the flight data if (flight != null) { try { + FlightDataBranch branch = new FlightDataBranch(); + if (flight.getBranchCount() > 0) { + branch = flight.getBranch(0); + } final Unit distanceUnit = UnitGroup.UNITS_DISTANCE.getDefaultUnit(); final Unit velocityUnit = UnitGroup.UNITS_VELOCITY.getDefaultUnit(); final Unit flightTimeUnit = UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit(); @@ -482,7 +538,7 @@ public class DesignReport { labelTable.addCell(ITextHelper.createCell(flightTimeUnit.toStringUnit(flight.getTimeToApogee()), 2, 2)); labelTable.addCell(ITextHelper.createCell(OPTIMUM_DELAY, 2, 2)); - labelTable.addCell(ITextHelper.createCell(flightTimeUnit.toStringUnit(flight.getBranch(0).getOptimumDelay()), 2, 2)); + labelTable.addCell(ITextHelper.createCell(flightTimeUnit.toStringUnit(branch.getOptimumDelay()), 2, 2)); labelTable.addCell(ITextHelper.createCell(VELOCITY_OFF_PAD, 2, 2)); labelTable.addCell(ITextHelper.createCell(velocityUnit.toStringUnit(flight.getLaunchRodVelocity()), 2, 2)); @@ -520,22 +576,160 @@ public class DesignReport { private FlightData findSimulation(final FlightConfigurationId motorId, List simulations) { // Perform flight simulation FlightData flight = null; - try { - for (int i = 0; i < simulations.size(); i++) { - Simulation simulation = simulations.get(i); - if (Utils.equals(simulation.getId(), motorId)) { - simulation = simulation.copy(); - simulation.simulate(); - flight = simulation.getSimulatedData(); - break; - } + for (int i = 0; i < simulations.size(); i++) { + Simulation simulation = simulations.get(i); + if (Utils.equals(simulation.getId(), motorId)) { + flight = simulation.getSimulatedData(); + break; } - } catch (SimulationException e1) { - // Ignore } return flight; } + /** + * Returns a list of Simulations to use for printing the design report + * for the rocket and optionally re-run out of date simulations. + * + * If the user has selected to not run any simulations, this method will + * simply return the simulations found in the OpenRocketDocument. + * + * If the user has selected to run simulations, this method will identify + * any simulations which are not up to date and re-run them. + * + * @return a list of Simulations to include in the DesignReport. + */ + protected List getSimulations() { + List simulations = rocketDocument.getSimulations(); + if (!runOutOfDateSimulations) { + log.debug("Using current simulations for rocket."); + return simulations; + } + + ArrayList simulationsToRun = new ArrayList(); + ArrayList upToDateSimulations = new ArrayList(); + for (Simulation simulation : simulations) { + boolean simulate = false; + boolean copy = !this.updateExistingSimulations; + + switch (simulation.getStatus()) { + case CANT_RUN: + log.warn("Simulation " + simulation.getId() + " has no motors, skipping"); + // Continue so we don't simulate + continue; + case UPTODATE: + log.trace("Simulation " + simulation.getId() + "is up to date, not running simulation"); + simulate = false; + break; + case NOT_SIMULATED: + case OUTDATED: + case LOADED: + log.trace("Running simulation for " + simulation.getId()); + simulate = true; + break; + case EXTERNAL: + log.trace("Simulation " + simulation.getId() + " is external. Using data provided"); + simulate = false; + break; + default: + log.trace("Running simulation for " + simulation.getId()); + simulate = true; + copy = true; + break; + } + + if (!simulate) { + upToDateSimulations.add(simulation); + } else if (copy) { + simulationsToRun.add(simulation.copy()); + } else { + simulationsToRun.add(simulation); + } + } + + /* Run any simulations that are pending a run. This is done via the + * SimulationRunDialog in order to provide user feedback. + */ + if (!simulationsToRun.isEmpty()) { + runSimulations(simulationsToRun); + upToDateSimulations.addAll(simulationsToRun); + } + + return upToDateSimulations; + } + + /** + * Runs the selected set of simulations. If a valid Window was provided when + * creating this DesignReport, this method will run simulations using the + * SimulationRunDialog in order to present status to the user. + * + * @param simulations a list of Simulations to run + */ + protected void runSimulations(List simulations) { + if (window != null) { + log.debug("Updating " + simulations.size() + "simulations using SimulationRunDialog"); + Simulation[] runMe = simulations.toArray(new Simulation[simulations.size()]); + new SimulationRunDialog(window, rocketDocument, runMe).setVisible(true); + } else { + /* This code is left for compatibility with any developers who are + * using the API to generate design reports. This may not be running + * graphically and the SimulationRunDialog may not be available for + * displaying progress information/updating simulations. + */ + log.debug("Updating simulations using thread pool"); + int cores = Runtime.getRuntime().availableProcessors(); + ThreadPoolExecutor executor = new ThreadPoolExecutor(cores, cores, 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(), + new SimulationRunnerThreadFactory()); + for (Simulation simulation : simulations) { + executor.execute(new RunSimulationTask(simulation)); + } + executor.shutdown(); + try { + /* Arbitrarily wait for at most 5 minutes for the simulation + * to complete. This seems like a long time, but in case there + * is a really long running simulation + */ + executor.awaitTermination(5, TimeUnit.MINUTES); + } catch (InterruptedException ie) { + + } + } + } + + private static class SimulationRunnerThreadFactory implements ThreadFactory { + private ThreadFactory factory = Executors.defaultThreadFactory(); + + @Override + public Thread newThread(Runnable r) { + Thread t = factory.newThread(r); + t.setDaemon(true); + return t; + } + } + + /** + * The RunSimulationTask is responsible for running simulations within the + * DesignReport when run outside of the SimulationRunDialog. + */ + private static class RunSimulationTask implements Runnable { + + private final Simulation simulation; + + public RunSimulationTask(final Simulation simulation) { + this.simulation = simulation; + } + + @Override + public void run() { + try { + simulation.simulate(); + } catch (SimulationException ex) { + log.error("Error simulating " + simulation.getId(), ex); + } + } + + } + /** * Strip [] brackets from a string. * diff --git a/swing/src/net/sf/openrocket/gui/print/PrintController.java b/swing/src/net/sf/openrocket/gui/print/PrintController.java index fd3f04a3c..e30581326 100644 --- a/swing/src/net/sf/openrocket/gui/print/PrintController.java +++ b/swing/src/net/sf/openrocket/gui/print/PrintController.java @@ -20,6 +20,7 @@ import net.sf.openrocket.gui.print.visitor.PageFitPrintStrategy; import net.sf.openrocket.gui.print.visitor.PartsDetailVisitorStrategy; import net.sf.openrocket.gui.print.visitor.TransitionStrategy; +import java.awt.Window; import java.io.IOException; import java.io.OutputStream; import java.util.Iterator; @@ -30,6 +31,33 @@ import java.util.Set; * file. */ public class PrintController { + + /** + * Used for displaying progress information when the Window + * is not null. + */ + private Window window = null; + + /** + * Sets the reference Window for displaying progress information + * for long running print operations. + * + * @param window the reference window + */ + public void setWindow(final Window window) { + this.window = window; + } + + /** + * Returns the reference Window for displaying progress information + * for long running print operations. + * + * @return the reference Window for displaying progress. May be null + * if no reference Window has been set. + */ + public Window getWindow() { + return this.window; + } /** * Print the selected components to a PDF document. @@ -39,9 +67,10 @@ public class PrintController { * @param outputFile the file being written to * @param settings the print settings * @param rotation the angle the rocket figure is rotated + * @param runSims determines whether to re-run out of date simulations or not */ public void print(OpenRocketDocument doc, Iterator toBePrinted, OutputStream outputFile, - PrintSettings settings, double rotation) { + PrintSettings settings, double rotation, boolean runSims) { Document idoc = new Document(getSize(settings)); PdfWriter writer = null; @@ -51,12 +80,7 @@ public class PrintController { writer.addViewerPreference(PdfName.PRINTSCALING, PdfName.NONE); writer.addViewerPreference(PdfName.PICKTRAYBYPDFSIZE, PdfBoolean.PDFTRUE); - try { - idoc.open(); - Thread.sleep(1000); - } - catch (InterruptedException e) { - } + idoc.open(); // Used to combine multiple components onto fewer sheets of paper PageFitPrintStrategy pageFitPrint = new PageFitPrintStrategy(idoc, writer); @@ -70,7 +94,7 @@ public class PrintController { switch (printableContext.getPrintable()) { case DESIGN_REPORT: - DesignReport dp = new DesignReport(doc, idoc, rotation); + DesignReport dp = new DesignReport(doc, idoc, rotation, runSims, true, this.window); dp.writeToDocument(writer); idoc.newPage(); break;