From a86f988b6988e421823f4454f6c68c8cc3730976 Mon Sep 17 00:00:00 2001 From: Bill Kuker Date: Wed, 15 Feb 2012 21:24:03 +0000 Subject: [PATCH] Include a figure of the ORK in the sim tab --- .../visual/openRocket/OneMotorDatabase.java | 4 + .../visual/openRocket/RocketPanel.java | 620 ++++++++++++++++++ .../visual/openRocket/RocketSimTable.java | 19 +- ivy.xml | 7 +- 4 files changed, 638 insertions(+), 12 deletions(-) create mode 100644 gpl/com/billkuker/rocketry/motorsim/visual/openRocket/RocketPanel.java diff --git a/gpl/com/billkuker/rocketry/motorsim/visual/openRocket/OneMotorDatabase.java b/gpl/com/billkuker/rocketry/motorsim/visual/openRocket/OneMotorDatabase.java index cdd31d6..3ec5cd4 100644 --- a/gpl/com/billkuker/rocketry/motorsim/visual/openRocket/OneMotorDatabase.java +++ b/gpl/com/billkuker/rocketry/motorsim/visual/openRocket/OneMotorDatabase.java @@ -59,12 +59,16 @@ public class OneMotorDatabase implements MotorDatabase { @Override public double getDiameter() { + if ( burn == null ) + return 0; return ((ICylindricalChamber) burn.getMotor().getChamber()).getOD() .doubleValue(SI.METER); } @Override public double getLength() { + if ( burn == null ) + return 0; return ((ICylindricalChamber) burn.getMotor().getChamber()) .getLength().doubleValue(SI.METER); } diff --git a/gpl/com/billkuker/rocketry/motorsim/visual/openRocket/RocketPanel.java b/gpl/com/billkuker/rocketry/motorsim/visual/openRocket/RocketPanel.java new file mode 100644 index 0000000..6079c2f --- /dev/null +++ b/gpl/com/billkuker/rocketry/motorsim/visual/openRocket/RocketPanel.java @@ -0,0 +1,620 @@ +package com.billkuker.rocketry.motorsim.visual.openRocket; + +import java.awt.Dimension; +import java.awt.event.InputEvent; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EventListener; +import java.util.EventObject; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSlider; +import javax.swing.JViewport; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.aerodynamics.AerodynamicCalculator; +import net.sf.openrocket.aerodynamics.BarrowmanCalculator; +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.gui.figureelements.CGCaret; +import net.sf.openrocket.gui.figureelements.CPCaret; +import net.sf.openrocket.gui.figureelements.Caret; +import net.sf.openrocket.gui.figureelements.RocketInfo; +import net.sf.openrocket.gui.main.SimulationWorker; +import net.sf.openrocket.gui.scalefigure.RocketFigure; +import net.sf.openrocket.gui.scalefigure.ScaleScrollPane; +import net.sf.openrocket.gui.util.SwingPreferences; +import net.sf.openrocket.masscalc.BasicMassCalculator; +import net.sf.openrocket.masscalc.MassCalculator; +import net.sf.openrocket.masscalc.MassCalculator.MassCalcType; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.SymmetricComponent; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.listeners.SimulationListener; +import net.sf.openrocket.simulation.listeners.system.ApogeeEndListener; +import net.sf.openrocket.simulation.listeners.system.InterruptListener; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.ChangeSource; +import net.sf.openrocket.util.Chars; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.StateChangeListener; + +/** + * A JPanel that contains a RocketFigure and buttons to manipulate the figure. + * + * @author Sampo Niskanen + */ +public class RocketPanel extends JPanel implements TreeSelectionListener, + ChangeSource { + + private static final long serialVersionUID = 1L; + + private final RocketFigure figure; + private final ScaleScrollPane scrollPane; + + private TreeSelectionModel selectionModel = null; + + /* Calculation of CP and CG */ + private AerodynamicCalculator aerodynamicCalculator; + private MassCalculator massCalculator; + + private final Configuration configuration; + + private Caret extraCP = null; + private Caret extraCG = null; + private RocketInfo extraText = null; + + private double cpAOA = Double.NaN; + private double cpTheta = Double.NaN; + private double cpMach = Double.NaN; + private double cpRoll = Double.NaN; + + // The functional ID of the rocket that was simulated + private int flightDataFunctionalID = -1; + private String flightDataMotorID = null; + + private SimulationWorker backgroundSimulationWorker = null; + + private List listeners = new ArrayList(); + + /** + * 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; + static { + backgroundSimulationExecutor = Executors.newFixedThreadPool( + SwingPreferences.getMaxThreadCount(), new ThreadFactory() { + private ThreadFactory factory = Executors + .defaultThreadFactory(); + + @Override + public Thread newThread(Runnable r) { + Thread t = factory.newThread(r); + t.setDaemon(true); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }); + } + + public RocketPanel(OpenRocketDocument document) { + + configuration = document.getDefaultConfiguration(); + + // TODO: FUTURE: calculator selection + aerodynamicCalculator = new BarrowmanCalculator(); + massCalculator = new BasicMassCalculator(); + + // Create figure and custom scroll pane + figure = new RocketFigure(configuration); + + scrollPane = new ScaleScrollPane(figure); + + scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE); + scrollPane.setFitting(true); + + createPanel(); + + configuration.addChangeListener(new StateChangeListener() { + @Override + public void stateChanged(EventObject e) { + // System.out.println("Configuration changed, calling updateFigure"); + updateExtras(); + figure.updateFigure(); + } + }); + } + + /** + * Creates the layout and components of the panel. + */ + private void createPanel() { + setLayout(new MigLayout("", "[shrink][grow]", "[shrink][grow]")); + + setPreferredSize(new Dimension(800, 250)); + + // // Create toolbar + + /* + * // Side/back buttons FigureTypeAction action = new + * FigureTypeAction(RocketFigure.TYPE_SIDE); // // Side view + * action.putValue(Action.NAME, + * trans.get("RocketPanel.FigTypeAct.Sideview")); // // Side view + * action.putValue(Action.SHORT_DESCRIPTION, + * trans.get("RocketPanel.FigTypeAct.ttip.Sideview")); JToggleButton + * toggle = new JToggleButton(action); add(toggle, "spanx, split"); + * + * action = new FigureTypeAction(RocketFigure.TYPE_BACK); // // Back + * view action.putValue(Action.NAME, + * trans.get("RocketPanel.FigTypeAct.Backview")); // // Back view + * action.putValue(Action.SHORT_DESCRIPTION, + * trans.get("RocketPanel.FigTypeAct.ttip.Backview")); toggle = new + * JToggleButton(action); add(toggle, "gap rel"); + */ + /* + * // Zoom level selector ScaleSelector scaleSelector = new + * ScaleSelector(scrollPane); add(scaleSelector); + */ + // add(new JLabel()); + + /* + * // Stage selector StageSelector stageSelector = new + * StageSelector(configuration); add(stageSelector, ""); + * + * add(new JLabel(), "wrap"); + */ + /* + * + * // Motor configuration selector //// Motor configuration: JLabel + * label = new JLabel(trans.get("RocketPanel.lbl.Motorcfg")); + * label.setHorizontalAlignment(JLabel.RIGHT); add(label, + * "growx, right"); add(new JComboBox(new + * MotorConfigurationModel(configuration)), "wrap"); + */ + configuration.setMotorConfigurationID(null); + + // add(new JLabel(), "growx, right"); + // add(new JLabel(), "wrap"); + + // Create slider and scroll pane + + DoubleModel theta = new DoubleModel(figure, "Rotation", + UnitGroup.UNITS_ANGLE, 0, 2 * Math.PI); + UnitSelector us = new UnitSelector(theta, true); + us.setHorizontalAlignment(JLabel.CENTER); + add(us, "alignx 50%, growx"); + + // Add the rocket figure + add(scrollPane, "grow, spany 2, wmin 300lp, hmin 100lp, wrap"); + + // Add rotation slider + // Minimum size to fit "360deg" + JLabel l = new JLabel("360" + Chars.DEGREE); + Dimension d = l.getPreferredSize(); + + add(new BasicSlider(theta.getSliderModel(0, 2 * Math.PI), + JSlider.VERTICAL, true), "ax 50%, wrap, width " + (d.width + 6) + + "px:null:null, growy"); + + /* + * // // Click to select    Shift+click to select other + * //    Double-click to edit    Click+drag to move + * infoMessage = new JLabel(trans.get("RocketPanel.lbl.infoMessage")); + * infoMessage.setFont(new Font("Sans Serif", Font.PLAIN, 9)); + * add(infoMessage, "skip, span, gapleft 25, wrap"); + */ + addExtras(); + } + + public RocketFigure getFigure() { + return figure; + } + + public AerodynamicCalculator getAerodynamicCalculator() { + return aerodynamicCalculator; + } + + public Configuration getConfiguration() { + return configuration; + } + + /** + * Get the center of pressure figure element. + * + * @return center of pressure info + */ + public Caret getExtraCP() { + return extraCP; + } + + /** + * Get the center of gravity figure element. + * + * @return center of gravity info + */ + public Caret getExtraCG() { + return extraCG; + } + + /** + * Get the extra text figure element. + * + * @return extra text that contains info about the rocket design + */ + public RocketInfo getExtraText() { + return extraText; + } + + public void setSelectionModel(TreeSelectionModel m) { + if (selectionModel != null) { + selectionModel.removeTreeSelectionListener(this); + } + selectionModel = m; + selectionModel.addTreeSelectionListener(this); + valueChanged((TreeSelectionEvent) null); // updates FigureParameters + } + + /** + * Return the angle of attack used in CP calculation. NaN signifies the + * default value of zero. + * + * @return the angle of attack used, or NaN. + */ + public double getCPAOA() { + return cpAOA; + } + + /** + * Set the angle of attack to be used in CP calculation. A value of NaN + * signifies that the default AOA (zero) should be used. + * + * @param aoa + * the angle of attack to use, or NaN + */ + public void setCPAOA(double aoa) { + if (MathUtil.equals(aoa, cpAOA) + || (Double.isNaN(aoa) && Double.isNaN(cpAOA))) + return; + cpAOA = aoa; + updateExtras(); + figure.updateFigure(); + fireChangeEvent(); + } + + public double getCPTheta() { + return cpTheta; + } + + public void setCPTheta(double theta) { + if (MathUtil.equals(theta, cpTheta) + || (Double.isNaN(theta) && Double.isNaN(cpTheta))) + return; + cpTheta = theta; + if (!Double.isNaN(theta)) + figure.setRotation(theta); + updateExtras(); + figure.updateFigure(); + fireChangeEvent(); + } + + public double getCPMach() { + return cpMach; + } + + public void setCPMach(double mach) { + if (MathUtil.equals(mach, cpMach) + || (Double.isNaN(mach) && Double.isNaN(cpMach))) + return; + cpMach = mach; + updateExtras(); + figure.updateFigure(); + fireChangeEvent(); + } + + public double getCPRoll() { + return cpRoll; + } + + public void setCPRoll(double roll) { + if (MathUtil.equals(roll, cpRoll) + || (Double.isNaN(roll) && Double.isNaN(cpRoll))) + return; + cpRoll = roll; + updateExtras(); + figure.updateFigure(); + fireChangeEvent(); + } + + @Override + public void addChangeListener(EventListener listener) { + listeners.add(0, listener); + } + + @Override + public void removeChangeListener(EventListener listener) { + listeners.remove(listener); + } + + protected void fireChangeEvent() { + EventObject e = new EventObject(this); + for (EventListener l : listeners) { + if (l instanceof StateChangeListener) { + ((StateChangeListener) l).stateChanged(e); + } + } + } + + /** + * 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. + */ + public static final int CYCLE_SELECTION_MODIFIER = InputEvent.SHIFT_DOWN_MASK; + + /** + * Updates the extra data included in the figure. Currently this includes + * the CP and CG carets. + */ + private WarningSet warnings = new WarningSet(); + + private void updateExtras() { + Coordinate cp, cg; + double cpx, cgx; + + // TODO: MEDIUM: User-definable conditions + FlightConditions conditions = new FlightConditions(configuration); + warnings.clear(); + + if (!Double.isNaN(cpMach)) { + conditions.setMach(cpMach); + extraText.setMach(cpMach); + } else { + conditions.setMach(Application.getPreferences().getDefaultMach()); + extraText.setMach(Application.getPreferences().getDefaultMach()); + } + + if (!Double.isNaN(cpAOA)) { + conditions.setAOA(cpAOA); + } else { + conditions.setAOA(0); + } + extraText.setAOA(cpAOA); + + if (!Double.isNaN(cpRoll)) { + conditions.setRollRate(cpRoll); + } else { + conditions.setRollRate(0); + } + + if (!Double.isNaN(cpTheta)) { + conditions.setTheta(cpTheta); + cp = aerodynamicCalculator.getCP(configuration, conditions, + warnings); + } else { + cp = aerodynamicCalculator.getWorstCP(configuration, conditions, + warnings); + } + extraText.setTheta(cpTheta); + + cg = massCalculator.getCG(configuration, MassCalcType.LAUNCH_MASS); + // System.out.println("CG computed as "+cg+ " CP as "+cp); + + if (cp.weight > 0.000001) + cpx = cp.x; + else + cpx = Double.NaN; + + if (cg.weight > 0.000001) + cgx = cg.x; + else + cgx = Double.NaN; + + // Length bound is assumed to be tight + double length = 0, diameter = 0; + Collection bounds = configuration.getBounds(); + if (!bounds.isEmpty()) { + double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY; + for (Coordinate c : bounds) { + if (c.x < minX) + minX = c.x; + if (c.x > maxX) + maxX = c.x; + } + length = maxX - minX; + } + + for (RocketComponent c : configuration) { + if (c instanceof SymmetricComponent) { + double d1 = ((SymmetricComponent) c).getForeRadius() * 2; + double d2 = ((SymmetricComponent) c).getAftRadius() * 2; + diameter = MathUtil.max(diameter, d1, d2); + } + } + + extraText.setCG(cgx); + extraText.setCP(cpx); + extraText.setLength(length); + extraText.setDiameter(diameter); + extraText.setMass(cg.weight); + extraText.setWarnings(warnings); + + if (figure.getType() == RocketFigure.TYPE_SIDE && length > 0) { + + // TODO: LOW: Y-coordinate and rotation + extraCP.setPosition(cpx * RocketFigure.EXTRA_SCALE, 0); + extraCG.setPosition(cgx * RocketFigure.EXTRA_SCALE, 0); + + } else { + + extraCP.setPosition(Double.NaN, Double.NaN); + extraCG.setPosition(Double.NaN, Double.NaN); + + } + + // ////// Flight simulation in background + + // Check whether to compute or not + if (!((SwingPreferences) Application.getPreferences()) + .computeFlightInBackground()) { + extraText.setFlightData(null); + extraText.setCalculatingData(false); + stopBackgroundSimulation(); + return; + } + + // Check whether data is already up to date + if (flightDataFunctionalID == configuration.getRocket() + .getFunctionalModID() + && flightDataMotorID == configuration.getMotorConfigurationID()) { + return; + } + + flightDataFunctionalID = configuration.getRocket().getFunctionalModID(); + flightDataMotorID = configuration.getMotorConfigurationID(); + + // Stop previous computation (if any) + stopBackgroundSimulation(); + + // Check that configuration has motors + if (!configuration.hasMotors()) { + extraText.setFlightData(FlightData.NaN_DATA); + extraText.setCalculatingData(false); + return; + } + + // Start calculation process + extraText.setCalculatingData(true); + + Rocket duplicate = (Rocket) configuration.getRocket().copy(); + Simulation simulation = ((SwingPreferences) Application + .getPreferences()).getBackgroundSimulation(duplicate); + simulation.getOptions().setMotorConfigurationID( + configuration.getMotorConfigurationID()); + + backgroundSimulationWorker = new BackgroundSimulationWorker(simulation); + backgroundSimulationExecutor.execute(backgroundSimulationWorker); + } + + /** + * Cancels the current background simulation worker, if any. + */ + private void stopBackgroundSimulation() { + if (backgroundSimulationWorker != null) { + backgroundSimulationWorker.cancel(true); + backgroundSimulationWorker = null; + } + } + + /** + * A SimulationWorker that simulates the rocket flight in the background and + * sets the results to the extra text when finished. The worker can be + * cancelled if necessary. + */ + private class BackgroundSimulationWorker extends SimulationWorker { + + public BackgroundSimulationWorker(Simulation sim) { + super(sim); + } + + @Override + protected FlightData doInBackground() { + + // Pause a little while to allow faster UI reaction + try { + Thread.sleep(300); + } catch (InterruptedException ignore) { + } + if (isCancelled() || backgroundSimulationWorker != this) + return null; + + return super.doInBackground(); + } + + @Override + protected void simulationDone() { + // Do nothing if cancelled + if (isCancelled() || backgroundSimulationWorker != this) + return; + + backgroundSimulationWorker = null; + extraText.setFlightData(simulation.getSimulatedData()); + extraText.setCalculatingData(false); + figure.repaint(); + } + + @Override + protected SimulationListener[] getExtraListeners() { + return new SimulationListener[] { InterruptListener.INSTANCE, + ApogeeEndListener.INSTANCE }; + } + + @Override + protected void simulationInterrupted(Throwable t) { + // Do nothing on cancel, set N/A data otherwise + if (isCancelled() || backgroundSimulationWorker != this) // Double-check + return; + + backgroundSimulationWorker = null; + extraText.setFlightData(FlightData.NaN_DATA); + extraText.setCalculatingData(false); + figure.repaint(); + } + } + + /** + * Adds the extra data to the figure. Currently this includes the CP and CG + * carets. + */ + private void addExtras() { + figure.clearRelativeExtra(); + extraCG = new CGCaret(0, 0); + extraCP = new CPCaret(0, 0); + extraText = new RocketInfo(configuration); + updateExtras(); + figure.addRelativeExtra(extraCP); + figure.addRelativeExtra(extraCG); + figure.addAbsoluteExtra(extraText); + } + + /** + * Updates the selection in the FigureParameters and repaints the figure. + * Ignores the event itself. + */ + @Override + public void valueChanged(TreeSelectionEvent e) { + TreePath[] paths = selectionModel.getSelectionPaths(); + if (paths == null) { + figure.setSelection(null); + return; + } + + RocketComponent[] components = new RocketComponent[paths.length]; + for (int i = 0; i < paths.length; i++) + components[i] = (RocketComponent) paths[i].getLastPathComponent(); + figure.setSelection(components); + } + +} diff --git a/gpl/com/billkuker/rocketry/motorsim/visual/openRocket/RocketSimTable.java b/gpl/com/billkuker/rocketry/motorsim/visual/openRocket/RocketSimTable.java index 445991c..2e7023a 100644 --- a/gpl/com/billkuker/rocketry/motorsim/visual/openRocket/RocketSimTable.java +++ b/gpl/com/billkuker/rocketry/motorsim/visual/openRocket/RocketSimTable.java @@ -13,7 +13,6 @@ import java.util.Set; import java.util.Vector; import javax.measure.unit.SI; -import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; @@ -70,7 +69,6 @@ public class RocketSimTable extends JPanel implements BurnWatcher, Burn b; } - class TM extends AbstractTableModel { private static final long serialVersionUID = 1L; @@ -199,13 +197,16 @@ public class RocketSimTable extends JPanel implements BurnWatcher, public void openRocket(File f) throws RocketLoadException { this.doc = new OpenRocketLoader().load(f); - JPanel rocketInfo = new JPanel(); - JLabel name = new JLabel("File: " + f.getAbsolutePath()); - rocketInfo.add(name); - add(rocketInfo, BorderLayout.NORTH); - + // JPanel rocketInfo = new JPanel(); + // JLabel name = new JLabel("File: " + f.getAbsolutePath()); + // rocketInfo.add(name); + // add(rocketInfo, BorderLayout.NORTH); + + RocketPanel p = new RocketPanel(this.doc); + add(p, BorderLayout.NORTH); + entries = new Vector(); - for ( Burn b : burns ){ + for (Burn b : burns) { entries.add(toEntry(b)); } tm.fireTableDataChanged(); @@ -255,7 +256,7 @@ public class RocketSimTable extends JPanel implements BurnWatcher, public void replace(Burn oldBurn, Burn newBurn) { burns.add(newBurn); burns.remove(oldBurn); - + if (doc == null) { return; // TODO, deal with changing rockets } diff --git a/ivy.xml b/ivy.xml index c4b3005..d39f400 100644 --- a/ivy.xml +++ b/ivy.xml @@ -9,9 +9,6 @@ - - - + + + -- 2.47.2