DGP - 1st printing
authorrodinia814 <rodinia814@180e2498-e6e9-4542-8430-84ac67f01cd8>
Tue, 7 Sep 2010 04:46:39 +0000 (04:46 +0000)
committerrodinia814 <rodinia814@180e2498-e6e9-4542-8430-84ac67f01cd8>
Tue, 7 Sep 2010 04:46:39 +0000 (04:46 +0000)
git-svn-id: https://openrocket.svn.sourceforge.net/svnroot/openrocket/branches/printing@79 180e2498-e6e9-4542-8430-84ac67f01cd8

66 files changed:
.classpath
src/net/sf/openrocket/aerodynamics/barrowman/LaunchLugCalc.java
src/net/sf/openrocket/file/openrocket/OpenRocketLoader.java
src/net/sf/openrocket/file/openrocket/savers/BodyTubeSaver.java
src/net/sf/openrocket/file/openrocket/savers/LaunchLugSaver.java
src/net/sf/openrocket/file/rocksim/BodyTubeHandler.java
src/net/sf/openrocket/file/rocksim/LaunchLugHandler.java
src/net/sf/openrocket/file/rocksim/ParachuteHandler.java
src/net/sf/openrocket/gui/components/ColorChooser.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/BodyTubeConfig.java
src/net/sf/openrocket/gui/configdialog/LaunchLugConfig.java
src/net/sf/openrocket/gui/dialogs/PrintDialog.java [new file with mode: 0644]
src/net/sf/openrocket/gui/dialogs/PrintPanel.java [new file with mode: 0644]
src/net/sf/openrocket/gui/figureelements/RocketInfo.java
src/net/sf/openrocket/gui/main/BasicFrame.java
src/net/sf/openrocket/gui/print/DesignReport.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/ITextHelper.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/OpenRocketPrintable.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/PDFPrintStreamDoc.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/PaperSize.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/PrintController.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/PrintFigure.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/PrintSimulationWorker.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/PrintUnit.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/PrintUtilities.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/PrintableContext.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/PrintableFinSet.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/TemplateProperties.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/components/CheckBoxNode.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/components/CheckTreeCellRenderer.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/components/CheckTreeManager.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/components/CheckTreeSelectionModel.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/components/RocketPrintTree.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/visitor/BaseVisitorStrategy.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/visitor/FinSetVisitorStrategy.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/visitor/MotorMountVisitorStrategy.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/visitor/PartsDetailVisitorStrategy.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/visitor/PartsListVisitorStrategy.java [new file with mode: 0644]
src/net/sf/openrocket/gui/print/visitor/StageVisitorStrategy.java [new file with mode: 0644]
src/net/sf/openrocket/gui/rocketfigure/BodyTubeShapes.java
src/net/sf/openrocket/gui/rocketfigure/LaunchLugShapes.java
src/net/sf/openrocket/gui/scalefigure/RocketFigure.java
src/net/sf/openrocket/gui/scalefigure/RocketPanel.java
src/net/sf/openrocket/rocketcomponent/BodyComponent.java
src/net/sf/openrocket/rocketcomponent/BodyTube.java
src/net/sf/openrocket/rocketcomponent/CenteringRing.java
src/net/sf/openrocket/rocketcomponent/Coaxial.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/ComponentVisitor.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/ComponentVisitorStrategy.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/EllipticalFinSet.java
src/net/sf/openrocket/rocketcomponent/ExternalComponent.java
src/net/sf/openrocket/rocketcomponent/FreeformFinSet.java
src/net/sf/openrocket/rocketcomponent/InnerTube.java
src/net/sf/openrocket/rocketcomponent/LaunchLug.java
src/net/sf/openrocket/rocketcomponent/NoseCone.java
src/net/sf/openrocket/rocketcomponent/RadiusRingComponent.java
src/net/sf/openrocket/rocketcomponent/RingComponent.java
src/net/sf/openrocket/rocketcomponent/Rocket.java
src/net/sf/openrocket/rocketcomponent/RocketComponent.java
src/net/sf/openrocket/rocketcomponent/Stage.java
src/net/sf/openrocket/rocketcomponent/Transition.java
src/net/sf/openrocket/rocketcomponent/TrapezoidFinSet.java
src/net/sf/openrocket/rocketcomponent/Visitable.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/Visitor.java [new file with mode: 0644]
src/net/sf/openrocket/util/TestRockets.java
test/net/sf/openrocket/file/rocksim/LaunchLugHandlerTest.java

index c71fbb9c8c6e4b56d985972e2a1b8e4240cb0df6..d1b79c768119276a248cc797932dbb1631b49774 100644 (file)
@@ -18,5 +18,6 @@
                </accessrules>
        </classpathentry>
        <classpathentry kind="lib" path="lib/miglayout15-swing.jar"/>
+       <classpathentry kind="lib" path="lib/iText-5.0.2.jar"/>
        <classpathentry kind="output" path="bin"/>
 </classpath>
index 88cf91ffe78325c820df46b0d6da1e90d3516efc..cd7fea0125e904c192d80055ba6169c22b443868 100644 (file)
@@ -16,10 +16,10 @@ public class LaunchLugCalc extends RocketComponentCalc {
                super(component);
                
                LaunchLug lug = (LaunchLug)component;
-               double ld = lug.getLength() / (2*lug.getRadius());
+               double ld = lug.getLength() / (2*lug.getOuterRadius());
                
                CDmul = Math.max(1.3 - ld, 1);
-               refArea = Math.PI * MathUtil.pow2(lug.getRadius()) - 
+               refArea = Math.PI * MathUtil.pow2(lug.getOuterRadius()) - 
                                  Math.PI * MathUtil.pow2(lug.getInnerRadius()) * Math.max(1 - ld, 0);
        }
 
index 090ee3ea0e4cae923ba1a7637133b92bafdfc6d0..24fddbbdac81f6cffb6523212da2f1e4f8835456 100644 (file)
@@ -1,21 +1,12 @@
 package net.sf.openrocket.file.openrocket;
 
-import java.awt.Color;
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-
 import net.sf.openrocket.aerodynamics.Warning;
 import net.sf.openrocket.aerodynamics.WarningSet;
 import net.sf.openrocket.database.Databases;
 import net.sf.openrocket.document.OpenRocketDocument;
 import net.sf.openrocket.document.Simulation;
-import net.sf.openrocket.document.StorageOptions;
 import net.sf.openrocket.document.Simulation.Status;
+import net.sf.openrocket.document.StorageOptions;
 import net.sf.openrocket.file.RocketLoadException;
 import net.sf.openrocket.file.RocketLoader;
 import net.sf.openrocket.file.simplesax.ElementHandler;
@@ -35,7 +26,9 @@ import net.sf.openrocket.rocketcomponent.Clusterable;
 import net.sf.openrocket.rocketcomponent.EllipticalFinSet;
 import net.sf.openrocket.rocketcomponent.EngineBlock;
 import net.sf.openrocket.rocketcomponent.ExternalComponent;
+import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish;
 import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.FinSet.TabRelativePosition;
 import net.sf.openrocket.rocketcomponent.FreeformFinSet;
 import net.sf.openrocket.rocketcomponent.IllegalFinPointException;
 import net.sf.openrocket.rocketcomponent.InnerTube;
@@ -52,6 +45,7 @@ import net.sf.openrocket.rocketcomponent.ReferenceType;
 import net.sf.openrocket.rocketcomponent.RingComponent;
 import net.sf.openrocket.rocketcomponent.Rocket;
 import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.RocketComponent.Position;
 import net.sf.openrocket.rocketcomponent.ShockCord;
 import net.sf.openrocket.rocketcomponent.Stage;
 import net.sf.openrocket.rocketcomponent.Streamer;
@@ -61,25 +55,30 @@ import net.sf.openrocket.rocketcomponent.ThicknessRingComponent;
 import net.sf.openrocket.rocketcomponent.Transition;
 import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
 import net.sf.openrocket.rocketcomponent.TubeCoupler;
-import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish;
-import net.sf.openrocket.rocketcomponent.FinSet.TabRelativePosition;
-import net.sf.openrocket.rocketcomponent.RocketComponent.Position;
 import net.sf.openrocket.simulation.FlightData;
 import net.sf.openrocket.simulation.FlightDataBranch;
 import net.sf.openrocket.simulation.FlightDataType;
 import net.sf.openrocket.simulation.FlightEvent;
-import net.sf.openrocket.simulation.GUISimulationConditions;
 import net.sf.openrocket.simulation.FlightEvent.Type;
+import net.sf.openrocket.simulation.GUISimulationConditions;
 import net.sf.openrocket.startup.Application;
 import net.sf.openrocket.unit.UnitGroup;
 import net.sf.openrocket.util.BugException;
 import net.sf.openrocket.util.Coordinate;
 import net.sf.openrocket.util.LineStyle;
 import net.sf.openrocket.util.Reflection;
-
 import org.xml.sax.InputSource;
 import org.xml.sax.SAXException;
 
+import java.awt.Color;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
 
 /**
  * Class that loads a rocket definition from an OpenRocket rocket file.
@@ -244,7 +243,7 @@ class DocumentConfig {
                
                // BodyTube
                setters.put("BodyTube:radius", new DoubleSetter(
-                               Reflection.findMethodStatic(BodyTube.class, "setRadius", double.class),
+                               Reflection.findMethodStatic(BodyTube.class, "setOuterRadius", double.class),
                                "auto",
                                Reflection.findMethodStatic(BodyTube.class, "setRadiusAutomatic", boolean.class)));
                
@@ -329,7 +328,7 @@ class DocumentConfig {
                
                // LaunchLug
                setters.put("LaunchLug:radius", new DoubleSetter(
-                               Reflection.findMethodStatic(LaunchLug.class, "setRadius", double.class)));
+                               Reflection.findMethodStatic(LaunchLug.class, "setOuterRadius", double.class)));
                setters.put("LaunchLug:length", new DoubleSetter(
                                Reflection.findMethodStatic(LaunchLug.class, "setLength", double.class)));
                setters.put("LaunchLug:thickness", new DoubleSetter(
index 5caf289634cabb4bdc66805ed7b7f0f215d6e1c1..2ca8c88d8afb1786a7754f7be3314867a9e9b9d2 100644 (file)
@@ -25,7 +25,7 @@ public class BodyTubeSaver extends SymmetricComponentSaver {
                if (tube.isRadiusAutomatic())
                        elements.add("<radius>auto</radius>");
                else
-                       elements.add("<radius>" + tube.getRadius() + "</radius>");
+                       elements.add("<radius>" + tube.getOuterRadius() + "</radius>");
 
                if (tube.isMotorMount()) {
                        elements.addAll(motorMountParams(tube));
index 3008bcce97c8a4f8e6e3fc6cb20f6cde19a9a357..1598bc412e1ae96e57dd4bbf0a140fc93de96ae3 100644 (file)
@@ -1,10 +1,10 @@
 package net.sf.openrocket.file.openrocket.savers;
 
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+
 import java.util.ArrayList;
 import java.util.List;
 
-import net.sf.openrocket.rocketcomponent.LaunchLug;
-
 
 public class LaunchLugSaver extends ExternalComponentSaver {
 
@@ -25,7 +25,7 @@ public class LaunchLugSaver extends ExternalComponentSaver {
                super.addParams(c, elements);
                LaunchLug lug = (LaunchLug) c;
 
-               elements.add("<radius>" + lug.getRadius() + "</radius>");
+               elements.add("<radius>" + lug.getOuterRadius() + "</radius>");
                elements.add("<length>" + lug.getLength() + "</length>");
                elements.add("<thickness>" + lug.getThickness() + "</thickness>");
                elements.add("<radialdirection>" + (lug.getRadialDirection()*180.0/Math.PI) + "</radialdirection>");
index a645827f37a23b54bda45bd1fb52eefb9b6ebe78..f8bc483841ccae2c72c351e038d939c819d4ace0 100644 (file)
@@ -54,7 +54,7 @@ class BodyTubeHandler extends BaseHandler<BodyTube> {
 
         try {
             if ("OD".equals(element)) {
-                bodyTube.setRadius(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS);
+                bodyTube.setOuterRadius(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS);
             }
             if ("ID".equals(element)) {
                 final double r = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS;
index 049feedd8579b4fd05f6a629938a1bde28cf9428..6d981ae4a542e75ead7efe9ece19d2331d6b410e 100644 (file)
@@ -53,7 +53,7 @@ class LaunchLugHandler extends PositionDependentHandler<LaunchLug> {
 
         try {
             if ("OD".equals(element)) {
-                lug.setRadius(Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS));
+                lug.setOuterRadius(Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS));
             }
             if ("ID".equals(element)) {
                 lug.setInnerRadius(Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS));
index 44418faefe05ea15f86ae8b9d17c8be58fcac82d..6d739eb04ca060df5b2ea8209731b3ba24fdcc6b 100644 (file)
@@ -68,7 +68,7 @@ class ParachuteHandler extends RecoveryDeviceHandler<Parachute> {
                 double packed;
                 RocketComponent parent = chute.getParent();
                 if (parent instanceof BodyTube) {
-                    packed = ((BodyTube) parent).getRadius() * 0.9;
+                    packed = ((BodyTube) parent).getOuterRadius() * 0.9;
                 }
                 else if (parent instanceof InnerTube) {
                     packed = ((InnerTube) parent).getInnerRadius() * 0.9;
diff --git a/src/net/sf/openrocket/gui/components/ColorChooser.java b/src/net/sf/openrocket/gui/components/ColorChooser.java
new file mode 100644 (file)
index 0000000..e149e3c
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * ColorChooser.java
+ */
+package net.sf.openrocket.gui.components;
+
+import javax.swing.JButton;
+import javax.swing.JColorChooser;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+import javax.swing.SwingUtilities;
+import javax.swing.colorchooser.ColorSelectionModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import java.awt.Color;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+/**
+ * A panel implementation of a color chooser.  The panel has a label and a textfield.  The label identifies 
+ * what the color is to be used for (the purpose), and the textfield is uneditable but has its background set
+ * to the currently chosen color as a way of visualizing the color.
+ * 
+ * The chosen color may be retrieved via a call to getCurrentColor.
+ */
+public class ColorChooser extends JPanel {
+
+    private static final String COLOR_CHOOSER_BUTTON_LABEL = "Color";
+
+    // The currently selected color 
+    private Color curColor;
+
+    final JColorChooser chooser;
+    final JLabel label;
+    final JTextField field;
+    final JPanel p;
+
+    /**
+     * Construct a color chooser as a panel.
+     * l
+     * @param parent     the parent panel to which this component will be added
+     * @param theChooser the delegated color chooser; the initial color taken from this chooser
+     * @param theLabel   the label used as the 'purpose' of the color; placed next to a textfield
+     */
+    public ColorChooser (JPanel parent, JColorChooser theChooser, final String theLabel) {
+        p = parent;
+        chooser = theChooser;
+        chooser.setPreviewPanel(this);
+        // Initialize the currently selected color 
+        curColor = chooser.getColor();
+        label = new JLabel(theLabel + ":");
+
+        parent.add(label, "align right");
+        field = new JTextField();
+        field.setEditable(false);
+        field.setBackground(curColor);
+        parent.add(field, "width 50:100:100");
+
+        final JButton button = new JButton(COLOR_CHOOSER_BUTTON_LABEL);
+
+        ActionListener actionListener = new ActionListener() {
+            public void actionPerformed (ActionEvent actionEvent) {
+                chooser.updateUI();
+
+                final JDialog dialog = JColorChooser.createDialog(null,
+                                                                  theLabel, true,
+                                                                  chooser,
+                                                                  null, null);
+
+                // Wait until current event dispatching completes before showing
+                // dialog
+                Runnable showDialog = new Runnable() {
+                    public void run () {
+                        dialog.show();
+                    }
+                };
+                SwingUtilities.invokeLater(showDialog);
+            }
+        };
+        button.addActionListener(actionListener);
+        parent.add(button, "wrap");
+
+        // Add listener on model to detect changes to selected color 
+        ColorSelectionModel model = chooser.getSelectionModel();
+        model.addChangeListener(new ChangeListener() {
+            public void stateChanged (ChangeEvent evt) {
+                ColorSelectionModel model = (ColorSelectionModel) evt.getSource();
+                // Get the new color value 
+                curColor = model.getSelectedColor();
+                field.setBackground(curColor);
+            }
+        });
+    }
+
+    /**
+     * Get the user-selected color.
+     * 
+     * @return  the current color
+     */
+    public Color getCurrentColor () {
+        return curColor;
+    }
+}
\ No newline at end of file
index 874111a286e8addc00e79a3501d021b671e44ce6..ebc490ac98d02a50d276945a5fff7afbd4be9bf8 100644 (file)
@@ -1,11 +1,6 @@
 package net.sf.openrocket.gui.configdialog;
 
 
-import javax.swing.JCheckBox;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JSpinner;
-
 import net.miginfocom.swing.MigLayout;
 import net.sf.openrocket.gui.SpinnerEditor;
 import net.sf.openrocket.gui.adaptors.BooleanModel;
@@ -17,6 +12,11 @@ import net.sf.openrocket.rocketcomponent.BodyTube;
 import net.sf.openrocket.rocketcomponent.RocketComponent;
 import net.sf.openrocket.unit.UnitGroup;
 
+import javax.swing.JCheckBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+
 public class BodyTubeConfig extends RocketComponentConfig {
 
        private MotorConfig motorConfigPane = null;
@@ -42,7 +42,7 @@ public class BodyTubeConfig extends RocketComponentConfig {
                //// Body tube diameter
                panel.add(new JLabel("Outer diameter:"));
 
-               DoubleModel od  = new DoubleModel(component,"Radius",2,UnitGroup.UNITS_LENGTH,0);
+               DoubleModel od  = new DoubleModel(component,"OuterRadius",2,UnitGroup.UNITS_LENGTH,0);
                // Diameter = 2*Radius
 
                spin = new JSpinner(od.getSpinnerModel());
index 909c5d3156ef4d2081c6db2fcf8098049910f05f..893917a6b04f88c582430ac68fe554bb52615a5b 100644 (file)
@@ -1,11 +1,6 @@
 package net.sf.openrocket.gui.configdialog;
 
 
-import javax.swing.JComboBox;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JSpinner;
-
 import net.miginfocom.swing.MigLayout;
 import net.sf.openrocket.gui.SpinnerEditor;
 import net.sf.openrocket.gui.adaptors.DoubleModel;
@@ -16,6 +11,11 @@ import net.sf.openrocket.material.Material;
 import net.sf.openrocket.rocketcomponent.RocketComponent;
 import net.sf.openrocket.unit.UnitGroup;
 
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+
 public class LaunchLugConfig extends RocketComponentConfig {
 
        private MotorConfig motorConfigPane = null;
@@ -44,7 +44,7 @@ public class LaunchLugConfig extends RocketComponentConfig {
                //// Body tube diameter
                panel.add(new JLabel("Outer diameter:"));
 
-               DoubleModel od  = new DoubleModel(component,"Radius",2,UnitGroup.UNITS_LENGTH,0);
+               DoubleModel od  = new DoubleModel(component,"OuterRadius",2,UnitGroup.UNITS_LENGTH,0);
                // Diameter = 2*Radius
 
                spin = new JSpinner(od.getSpinnerModel());
diff --git a/src/net/sf/openrocket/gui/dialogs/PrintDialog.java b/src/net/sf/openrocket/gui/dialogs/PrintDialog.java
new file mode 100644 (file)
index 0000000..a9769ee
--- /dev/null
@@ -0,0 +1,270 @@
+/*
+ * PrintDialog.java
+ */
+package net.sf.openrocket.gui.dialogs;
+
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.gui.print.PDFPrintStreamDoc;
+import net.sf.openrocket.gui.print.PrintUtilities;
+import sun.print.ServiceDialog;
+
+import javax.print.DocFlavor;
+import javax.print.DocPrintJob;
+import javax.print.PrintException;
+import javax.print.PrintService;
+import javax.print.PrintServiceLookup;
+import javax.print.attribute.Attribute;
+import javax.print.attribute.AttributeSet;
+import javax.print.attribute.HashPrintRequestAttributeSet;
+import javax.print.attribute.PrintRequestAttributeSet;
+import javax.print.attribute.standard.Destination;
+import javax.print.attribute.standard.Fidelity;
+import javax.swing.JDialog;
+import javax.swing.JMenu;
+import javax.swing.JTabbedPane;
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Dialog;
+import java.awt.GraphicsConfiguration;
+import java.awt.GraphicsDevice;
+import java.awt.GraphicsEnvironment;
+import java.awt.HeadlessException;
+import java.awt.Rectangle;
+import java.io.ByteArrayOutputStream;
+
+/**
+ * This class is not a dialog by inheritance, but is by delegation.  It front-ends a java print dialog by
+ * augmenting it with application specific (rocket) settings.
+ */
+public class PrintDialog {
+
+    /**
+     * The service UI dialog.
+     */
+    private ServiceDialog dialog;
+
+    /**
+     * A javax doc flavor specific for printing PDF documents.
+     */
+    private static final DocFlavor.INPUT_STREAM PDF = DocFlavor.INPUT_STREAM.PDF;
+
+    /**
+     * Construct a print dialog using an Open Rocket document - which contains the rocket data to ultimately be
+     * printed.
+     *
+     * @param orDocument the rocket container
+     */
+    public PrintDialog (OpenRocketDocument orDocument) {
+        PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
+        PrintService svc = PrintServiceLookup.lookupDefaultPrintService();
+        PrintRequestAttributeSet attrs = new HashPrintRequestAttributeSet();
+        attrs.add(PrintUtilities.getDefaultMedia().getMediaSizeName());
+
+        final PrintPanel panel = new PrintPanel(orDocument, this);
+        PrintService ps = printDialog(null, 100, 100, services, svc, PDF, attrs, panel);
+        if (ps != null) {
+            DocPrintJob dpj = ps.createPrintJob();
+            try {
+                System.err.println(attrs.size());
+                ByteArrayOutputStream baos = panel.generateReport();
+                dpj.print(new PDFPrintStreamDoc(baos, null), attrs);
+            }
+            catch (PrintException e) {
+                e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
+            }
+        }
+    }
+
+    /**
+     * Get the set of attributes from the service ui print dialog.
+     *
+     * @return a set of print attributes
+     */
+    PrintRequestAttributeSet getAttributes () {
+        return dialog.getAttributes();
+    }
+
+    /**
+     * Get the service ui dialog.  This is the actual dialog that gets displayed - we co-opt it for adding a rocket
+     * specific tab.
+     *
+     * @return the Java service ui print dialog
+     */
+    JDialog getDialog () {
+        return dialog;
+    }
+
+    /**
+     * Mimics the ServiceUI.printDialog method, but with enhancements for our own print settings tab.
+     *
+     * @param gc             used to select screen. null means primary or default screen.
+     * @param x              location of dialog including border in screen coordinates
+     * @param y              location of dialog including border in screen coordinates
+     * @param services       to be browsable, must be non-null.
+     * @param defaultService - initial PrintService to display.
+     * @param flavor         - the flavor to be printed, or null.
+     * @param attributes     on input is the initial application supplied preferences. This cannot be null but may be
+     *                       empty. On output the attributes reflect changes made by the user.
+     * @param addnl          a panel to be added, as a tab, to the internal tabbed pane of the resulting print dialog
+     *
+     * @return print service selected by the user, or null if the user cancelled the dialog.
+     *
+     * @throws HeadlessException        if GraphicsEnvironment.isHeadless() returns true.
+     * @throws IllegalArgumentException if services is null or empty, or attributes is null, or the initial PrintService
+     *                                  is not in the list of browsable services.
+     */
+    private PrintService printDialog (GraphicsConfiguration gc,
+                                      int x, int y,
+                                      PrintService[] services,
+                                      PrintService defaultService,
+                                      DocFlavor flavor,
+                                      PrintRequestAttributeSet attributes,
+                                      PrintPanel addnl)
+            throws HeadlessException {
+        int defaultIndex = -1;
+
+        if (GraphicsEnvironment.isHeadless()) {
+            throw new HeadlessException();
+        }
+        else if ((services == null) || (services.length == 0)) {
+            throw new IllegalArgumentException("services must be non-null " +
+                                               "and non-empty");
+        }
+        else if (attributes == null) {
+            throw new IllegalArgumentException("attributes must be non-null");
+        }
+
+        if (defaultService != null) {
+            for (int i = 0; i < services.length; i++) {
+                if (services[i].equals(defaultService)) {
+                    defaultIndex = i;
+                    break;
+                }
+            }
+
+            if (defaultIndex < 0) {
+                throw new IllegalArgumentException("services must contain " +
+                                                   "defaultService");
+            }
+        }
+        else {
+            defaultIndex = 0;
+        }
+
+        Rectangle gcBounds = (gc == null) ? GraphicsEnvironment.
+                getLocalGraphicsEnvironment().getDefaultScreenDevice().
+                getDefaultConfiguration().getBounds() : gc.getBounds();
+
+        dialog = new ServiceDialog(gc,
+                                   x + gcBounds.x,
+                                   y + gcBounds.y,
+                                   services, defaultIndex,
+                                   flavor, attributes,
+                                   (Dialog) null);
+        Rectangle dlgBounds = dialog.getBounds();
+
+        // get union of all GC bounds
+        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
+        GraphicsDevice[] gs = ge.getScreenDevices();
+        for (GraphicsDevice g : gs) {
+            gcBounds = gcBounds.union(g.getDefaultConfiguration().getBounds());
+        }
+
+        // if portion of dialog is not within the gc boundary
+        if (!gcBounds.contains(dlgBounds)) {
+            // put in the center relative to parent frame/dialog
+            dialog.setLocationRelativeTo(null);
+        }
+        if (addnl != null && addnl.getTitle() != null) {
+            JTabbedPane tp = (JTabbedPane) getDescendantOfClass(JTabbedPane.class, dialog);
+            tp.add(addnl, addnl.getTitle(), 0);
+            tp.setSelectedIndex(0);
+        }
+
+        dialog.setVisible(true);
+
+        if (dialog.getStatus() == ServiceDialog.APPROVE) {
+            PrintRequestAttributeSet newas = dialog.getAttributes();
+            Class dstCategory = Destination.class;
+            Class fdCategory = Fidelity.class;
+
+            if (attributes.containsKey(dstCategory) &&
+                !newas.containsKey(dstCategory)) {
+                attributes.remove(dstCategory);
+            }
+
+            attributes.addAll(newas);
+
+            Fidelity fd = (Fidelity) attributes.get(fdCategory);
+            if (fd != null) {
+                if (fd == Fidelity.FIDELITY_TRUE) {
+                    removeUnsupportedAttributes(dialog.getPrintService(),
+                                                flavor, attributes);
+                }
+            }
+            return dialog.getPrintService();
+        }
+        else {
+            return null;
+        }
+    }
+
+    private Component getDescendantOfClass (Class c, Container cont) {
+        if (c == null || cont == null) {
+            return null;
+        }
+        Component[] children = (cont instanceof JMenu)
+                               ? ((JMenu) cont).getMenuComponents()
+                               : cont.getComponents();
+        for (int i = 0, n = children.length; i < n; i++) {
+            Component comp = children[i];
+            if (c.isInstance(comp)) {
+                return comp;
+            }
+            comp = getDescendantOfClass(c, (Container) comp);
+            if (comp != null) {
+                return comp;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Removes any attributes from the given AttributeSet that are unsupported by the given PrintService/DocFlavor
+     * combination.
+     * 
+     * @param ps      the print service for which unsupported attributes will be determined
+     * @param flavor  the document flavor; PDF in our case
+     * @param aset    the set of attributes requested
+     */
+    private static void removeUnsupportedAttributes (PrintService ps,
+                                                     DocFlavor flavor,
+                                                     AttributeSet aset) {
+        AttributeSet asUnsupported = ps.getUnsupportedAttributes(flavor,
+                                                                 aset);
+
+        if (asUnsupported != null) {
+            Attribute[] usAttrs = asUnsupported.toArray();
+
+            for (Attribute usAttr : usAttrs) {
+                Class<? extends Attribute> category = usAttr.getCategory();
+
+                if (ps.isAttributeCategorySupported(category)) {
+                    Attribute attr =
+                            (Attribute) ps.getDefaultAttributeValue(category);
+
+                    if (attr != null) {
+                        aset.add(attr);
+                    }
+                    else {
+                        aset.remove(category);
+                    }
+                }
+                else {
+                    aset.remove(category);
+                }
+            }
+        }
+    }
+
+}
diff --git a/src/net/sf/openrocket/gui/dialogs/PrintPanel.java b/src/net/sf/openrocket/gui/dialogs/PrintPanel.java
new file mode 100644 (file)
index 0000000..ecd1fd8
--- /dev/null
@@ -0,0 +1,434 @@
+/*
+ * PrintPanel.java
+ */
+package net.sf.openrocket.gui.dialogs;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.gui.components.ColorChooser;
+import net.sf.openrocket.gui.print.PrintController;
+import net.sf.openrocket.gui.print.PrintUtilities;
+import net.sf.openrocket.gui.print.PrintableContext;
+import net.sf.openrocket.gui.print.TemplateProperties;
+import net.sf.openrocket.gui.print.components.CheckTreeManager;
+import net.sf.openrocket.gui.print.components.RocketPrintTree;
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.startup.Application;
+
+import javax.print.attribute.PrintRequestAttributeSet;
+import javax.print.attribute.standard.MediaSizeName;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JColorChooser;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JFileChooser;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.UIManager;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.filechooser.FileFilter;
+import javax.swing.tree.TreeNode;
+import javax.swing.tree.TreePath;
+import java.awt.Color;
+import java.awt.Desktop;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.Iterator;
+
+/**
+ * This class isolates the Swing components used to create a panel that is added to the standard Java print dialog.
+ */
+public class PrintPanel extends JPanel implements TreeSelectionListener {
+
+    private static final LogHelper log = Application.getLogger();
+
+    private final RocketPrintTree stagedTree;
+    private final RocketPrintTree noStagedTree;
+    private OpenRocketDocument rocDoc;
+    private RocketPrintTree currentTree;
+    private boolean bDesktopSupported = false;
+    private Desktop desktop;
+    private PrintDialog printDialog;
+
+    JButton previewButton;
+    JButton saveAsPDF;
+    
+    /**
+     * Constructor.
+     *
+     * @param orDocument the OR rocket container
+     * @param theParent  the OR parent print dialog
+     */
+    public PrintPanel (OpenRocketDocument orDocument, PrintDialog theParent) {
+
+        super(new MigLayout("fill, gap rel unrel"));
+
+        // before any Desktop APIs are used, first check whether the API is
+        // supported by this particular VM on this particular host
+        if (Desktop.isDesktopSupported()) {
+            bDesktopSupported = true;
+            desktop = Desktop.getDesktop();
+        }
+
+        printDialog = theParent;
+        rocDoc = orDocument;
+        Rocket rocket = orDocument.getRocket();
+
+        noStagedTree = RocketPrintTree.create(rocket.getName());
+        noStagedTree.setShowsRootHandles(false);
+        CheckTreeManager ctm = new net.sf.openrocket.gui.print.components.CheckTreeManager(noStagedTree);
+        ctm.addTreeSelectionListener(this);
+
+        final int stages = rocket.getStageCount();
+
+        if (stages > 1) {
+            stagedTree = RocketPrintTree.create(rocket.getName(), rocket.getChildren());
+            ctm = new CheckTreeManager(stagedTree);
+            stagedTree.setShowsRootHandles(false);
+            ctm.addTreeSelectionListener(this);
+        }
+        else {
+            stagedTree = noStagedTree;
+        }
+        currentTree = stagedTree;
+
+        final JScrollPane scrollPane = new JScrollPane(stagedTree);
+        add(scrollPane, "width 475!, wrap");
+
+        final JCheckBox sortByStage = new JCheckBox("Show By Stage");
+        sortByStage.setEnabled(stages > 1);
+        sortByStage.setSelected(stages > 1);
+        sortByStage.addActionListener(new ActionListener() {
+            public void actionPerformed (ActionEvent e) {
+                if (sortByStage.isEnabled()) {
+                    if (((JCheckBox) e.getSource()).isSelected()) {
+                        scrollPane.setViewportView(stagedTree);
+                        stagedTree.setExpandsSelectedPaths(true);
+                        currentTree = stagedTree;
+                    }
+                    else {
+                        scrollPane.setViewportView(noStagedTree);
+                        noStagedTree.setExpandsSelectedPaths(true);
+                        currentTree = noStagedTree;
+                    }
+                }
+            }
+        });
+        add(sortByStage, "wrap");
+
+        saveAsPDF = new JButton("Save as PDF");
+        saveAsPDF.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed (ActionEvent e) {
+                onSavePDF(PrintPanel.this);
+            }
+        });
+        add(saveAsPDF, "span 2, tag save");
+
+        previewButton = new JButton("Preview");
+        previewButton.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed (ActionEvent e) {
+                onPreview();
+            }
+        });
+        add(previewButton, "x 150");
+
+        JButton settingsButton = new JButton("Settings");
+        settingsButton.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed (ActionEvent e) {
+                PrintSettingsDialog settingsDialog = new PrintSettingsDialog(printDialog.getDialog());
+                settingsDialog.setVisible(true);
+            }
+        });
+        add(settingsButton, "x 400");
+
+        expandAll(currentTree, true);
+        if (currentTree != noStagedTree) {
+            expandAll(noStagedTree, true);
+        }
+        setVisible(true);
+    }
+
+    /**
+     * The title of the tab that gets displayed for this panel, when placed in the print dialog.
+     *
+     * @return a title
+     */
+    public String getTitle () {
+        return "Rocket";
+    }
+
+    @Override 
+    public void valueChanged (final TreeSelectionEvent e) {
+        final TreePath path = e.getNewLeadSelectionPath();
+        if (path != null){
+            previewButton.setEnabled(true);
+            saveAsPDF.setEnabled(true);
+        }
+        else {
+            previewButton.setEnabled(false);
+            saveAsPDF.setEnabled(false);
+        }
+    }
+
+    /**
+     * If expand is true, expands all nodes in the tree. Otherwise, collapses all nodes in the theTree.
+     *
+     * @param theTree   the tree to expand/contract
+     * @param expand expand if true, contract if not
+     */
+    public void expandAll (RocketPrintTree theTree, boolean expand) {
+        TreeNode root = (TreeNode) theTree.getModel().getRoot();
+        // Traverse theTree from root 
+        expandAll(theTree, new TreePath(root), expand);
+    }
+
+    /**
+     * Recursively walk a tree, and if expand is true, expands all nodes in the tree. Otherwise, collapses all nodes in
+     * the theTree.
+     *
+     * @param theTree   the tree to expand/contract
+     * @param parent the node to iterate/recurse over
+     * @param expand expand if true, contract if not
+     */
+    private void expandAll (RocketPrintTree theTree, TreePath parent, boolean expand) {
+        theTree.addSelectionPath(parent);
+        // Traverse children 
+        TreeNode node = (TreeNode) parent.getLastPathComponent();
+        if (node.getChildCount() >= 0) {
+            for (Enumeration e = node.children(); e.hasMoreElements();) {
+                TreeNode n = (TreeNode) e.nextElement();
+                TreePath path = parent.pathByAddingChild(n);
+                expandAll(theTree, path, expand);
+            }
+        }
+        // Expansion or collapse must be done bottom-up 
+        if (expand) {
+            theTree.expandPath(parent);
+        }
+        else {
+            theTree.collapsePath(parent);
+        }
+    }
+
+    /**
+     * Get a media size name (the name of a paper size).  If no page size is selected, it will default to the locale
+     * specific page size (LETTER in North America, A4 elsewhere).
+     *
+     * @return the name of a page size
+     */
+    private MediaSizeName getMediaSize () {
+        MediaSizeName paperSize = getMediaSize(printDialog.getAttributes());
+        if (paperSize == null) {
+            paperSize = PrintUtilities.getDefaultMedia().getMediaSizeName();
+        }
+        return paperSize;
+    }
+
+    /**
+     * Get the media size name (the name of a paper size) as selected by the user.
+     *
+     * @param atts the set of selected printer attributes
+     *
+     * @return a media size name, may be null
+     */
+    private MediaSizeName getMediaSize (PrintRequestAttributeSet atts) {
+        return (MediaSizeName) atts.get(javax.print.attribute.standard.Media.class);
+    }
+
+    /**
+     * Generate a report using a temporary file.  The file will be deleted upon JVM exit.
+     *
+     * @param paper the name of the paper size
+     *
+     * @return a file, populated with the "printed" output (the rocket info)
+     *
+     * @throws IOException thrown if the file could not be generated
+     */
+    private File generateReport (MediaSizeName paper) throws IOException {
+        final File f = File.createTempFile("oro", ".pdf");
+        f.deleteOnExit();
+        return generateReport(f, paper);
+    }
+
+    /**
+     * Generate a report to a specified file.
+     *
+     * @param f     the file to which rocket data will be written
+     * @param paper the name of the paper size
+     *
+     * @return a file, populated with the "printed" output (the rocket info)
+     *
+     * @throws IOException thrown if the file could not be generated
+     */
+    private File generateReport (File f, MediaSizeName paper) throws IOException {
+        Iterator<PrintableContext> toBePrinted = currentTree.getToBePrinted();
+        new PrintController().print(rocDoc, toBePrinted, new FileOutputStream(f), paper);
+        return f;
+    }
+
+    /**
+     * Generate a report to a byte array output stream.
+     *
+     * @return a stream populated with the "printed" output (the rocket info)
+     */
+    public ByteArrayOutputStream generateReport() {
+        Iterator<PrintableContext> toBePrinted = currentTree.getToBePrinted();
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new PrintController().print(rocDoc, toBePrinted, baos, getMediaSize ());
+        return baos;
+    }
+
+    /**
+     * Handler for when the Preview button is clicked.
+     */
+    private void onPreview () {
+        if (bDesktopSupported) {
+            try {
+                MediaSizeName paperSize = getMediaSize();
+                File f = generateReport(paperSize);
+                desktop.open(f);
+            }
+            catch (IOException e) {
+                log.error("Could not create temporary file for previewing.", e);
+                JOptionPane.showMessageDialog(this, "Could not create a temporary file for previewing.",
+                                              "Error creating file", JOptionPane.ERROR_MESSAGE);
+            }
+        }
+        else {
+            JOptionPane.showMessageDialog(this,
+                                          "Your environment does not support automatically opening the default PDF viewer.",
+                                          "Error creating file", JOptionPane.INFORMATION_MESSAGE);
+        }
+    }
+
+    /**
+     * Handler for when the "Save as PDF" button is clicked.
+     *
+     * @param p the component to parent the save dialog to
+     */
+    private void onSavePDF (JComponent p) {
+
+        JFileChooser chooser = new JFileChooser();
+        // Note: source for ExampleFileFilter can be found in FileChooserDemo,
+        // under the demo/jfc directory in the Java 2 SDK, Standard Edition.
+        FileFilter filter = new FileFilter() {
+
+            //Accept all directories and all pdf files.
+            public boolean accept (File f) {
+                return true;
+            }
+
+            //The description of this filter
+            public String getDescription () {
+                return "pdf";
+            }
+        };
+        chooser.setFileFilter(filter);
+        int returnVal = chooser.showSaveDialog(p);
+        if (returnVal == JFileChooser.APPROVE_OPTION) {
+
+            try {
+                String fname = chooser.getSelectedFile().getCanonicalPath();
+                if (!getExtension(fname).equals("pdf")) {
+                    fname = fname + ".pdf";
+                }
+                File f = new File(fname);
+                generateReport(f, getMediaSize());
+            }
+            catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * Get the extension of a file.
+     */
+    private static String getExtension (String s) {
+        String ext = null;
+        int i = s.lastIndexOf('.');
+
+        if (i > 0 && i < s.length() - 1) {
+            ext = s.substring(i + 1).toLowerCase();
+        }
+        return ext != null ? ext : "";
+    }
+}
+
+/**
+ * This class is a dialog for displaying advanced settings for printing rocket related info.
+ */
+class PrintSettingsDialog extends JDialog {
+
+    /**
+     * The fill color chooser.
+     */
+    private ColorChooser fill;
+
+    /**
+     * The line color chooser.
+     */
+    private ColorChooser line;
+
+    /**
+     * Construct a dialog for setting the advanced rocket print settings.
+     *
+     * @param parent the owning dialog
+     */
+    public PrintSettingsDialog (JDialog parent) {
+        super(parent, "Advanced Settings", true);
+        setLayout(new MigLayout("fill"));
+
+        JPanel settingsPanel = new JPanel();
+        settingsPanel.setLayout(new MigLayout("gap rel"));
+
+        fill = addColorChooser(settingsPanel, "Template Fill", TemplateProperties.getFillColor());
+        line = addColorChooser(settingsPanel, "Template Line", TemplateProperties.getLineColor());
+
+        settingsPanel.add(fill);
+        settingsPanel.add(line);
+
+        add(settingsPanel, "wrap");
+
+        JButton closeButton = new JButton("Close");
+        closeButton.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed (ActionEvent e) {
+                UIManager.put(TemplateProperties.TEMPLATE_FILL_COLOR_PROPERTY, fill.getCurrentColor());
+                UIManager.put(TemplateProperties.TEMPLATE_LINE_COLOR_PROPERTY, line.getCurrentColor());
+                dispose();
+            }
+        });
+        add(closeButton, "right, gapright para");
+
+        setSize(400, 200);
+    }
+
+    /**
+     * Add a color chooser to a panel.
+     *
+     * @param panel        the parent panel to add the color chooser.
+     * @param label        the label that indicates which color property is being changed
+     * @param initialColor the initial, or current, color to display
+     *
+     * @return a swing component containing a label, a colorized field, and a button that when clicked opens a color
+     *         chooser dialog
+     */
+    private ColorChooser addColorChooser (JPanel panel, String label, Color initialColor) {
+        final JColorChooser colorChooser = new JColorChooser(initialColor);
+        return new ColorChooser(panel, colorChooser, label);
+    }
+
+}
index f573e182b2d3e93d1d0d4fcfd9c2db9664ca9277..0e9ef165a3a1d17309b9cbe724c156a63e239aab 100644 (file)
@@ -1,6 +1,13 @@
 package net.sf.openrocket.gui.figureelements;
 
-import static net.sf.openrocket.util.Chars.*;
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Prefs;
 
 import java.awt.Color;
 import java.awt.Font;
@@ -9,13 +16,8 @@ import java.awt.Rectangle;
 import java.awt.font.GlyphVector;
 import java.awt.geom.Rectangle2D;
 
-import net.sf.openrocket.aerodynamics.Warning;
-import net.sf.openrocket.aerodynamics.WarningSet;
-import net.sf.openrocket.rocketcomponent.Configuration;
-import net.sf.openrocket.simulation.FlightData;
-import net.sf.openrocket.unit.UnitGroup;
-import net.sf.openrocket.util.MathUtil;
-import net.sf.openrocket.util.Prefs;
+import static net.sf.openrocket.util.Chars.ALPHA;
+import static net.sf.openrocket.util.Chars.THETA;
 
 
 /**
@@ -174,11 +176,11 @@ public class RocketInfo implements FigureElement {
                }
                
                GlyphVector cgValue = createText(
-                               UnitGroup.UNITS_LENGTH.getDefaultUnit().toStringUnit(cg));
+                getCg());
                GlyphVector cpValue = createText(
-                               UnitGroup.UNITS_LENGTH.getDefaultUnit().toStringUnit(cp));
+                getCp());
                GlyphVector stabValue = createText(
-                               stabilityUnits.getDefaultUnit().toStringUnit(cp-cg));
+                getStability());
                                
                GlyphVector cgText = createText("CG:  ");
                GlyphVector cpText = createText("CP:  ");
@@ -226,8 +228,84 @@ public class RocketInfo implements FigureElement {
 
        }
 
-       
-       private void drawWarnings() {
+    /**
+     * Get the mass, in default mass units.
+     * 
+     * @return the mass
+     */
+    public double getMass() {
+        return mass;
+    }
+
+    /**
+     * Get the mass in specified mass units.
+     * 
+     * @param u UnitGroup.MASS
+     * 
+     * @return the mass
+     */
+    public String getMass(Unit u) {
+        return u.toStringUnit(mass);
+    }
+    
+    /**
+     * Get the stability, in calibers.
+     * 
+     * @return  the current stability margin
+     */
+    public String getStability () {
+        return stabilityUnits.getDefaultUnit().toStringUnit(cp-cg);
+    }
+
+    /**
+     * Get the center of pressure in default length units.
+     * 
+     * @return  the distance from the tip to the center of pressure, in default length units
+     */
+    public String getCp () {
+        return getCp(UnitGroup.UNITS_LENGTH.getDefaultUnit());
+    }
+
+    /**
+     * Get the center of pressure in default length units.
+     * 
+     * @param u UnitGroup.LENGTH 
+     * 
+     * @return  the distance from the tip to the center of pressure, in default length units
+     */
+    public String getCp (Unit u) {
+        return u.toStringUnit(cp);
+    }
+
+    /**
+     * Get the center of gravity in default length units.
+     * 
+     * @return  the distance from the tip to the center of gravity, in default length units
+     */
+    public String getCg () {
+        return getCg(UnitGroup.UNITS_LENGTH.getDefaultUnit());
+    }
+
+    /**
+     * Get the center of gravity in specified length units.
+     * 
+     * @param u UnitGroup.LENGTH 
+     * @return  the distance from the tip to the center of gravity, in specified units
+     */
+    public String getCg (Unit u) {
+        return u.toStringUnit(cg);
+    }
+
+    /**
+     * Get the flight data for the current motor configuration.
+     * 
+     * @return flight data, or null
+     */
+    public FlightData getFlightData () {
+        return flightData;
+    }
+    
+    private void drawWarnings() {
                if (warnings == null || warnings.isEmpty())
                        return;
                
index aba5398635fdee8f248706c9b3f7c866a4f2e185..43c214c72e5eb31e2ff95e22bec2162b72fcfea0 100644 (file)
@@ -1,62 +1,5 @@
 package net.sf.openrocket.gui.main;
 
-import java.awt.Dimension;
-import java.awt.Font;
-import java.awt.Toolkit;
-import java.awt.Window;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.awt.event.ComponentAdapter;
-import java.awt.event.ComponentEvent;
-import java.awt.event.KeyEvent;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-import java.awt.event.MouseListener;
-import java.awt.event.WindowAdapter;
-import java.awt.event.WindowEvent;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-import java.lang.reflect.InvocationTargetException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.net.URLDecoder;
-import java.util.ArrayList;
-import java.util.concurrent.ExecutionException;
-
-import javax.swing.Action;
-import javax.swing.InputMap;
-import javax.swing.JButton;
-import javax.swing.JComponent;
-import javax.swing.JFileChooser;
-import javax.swing.JFrame;
-import javax.swing.JMenu;
-import javax.swing.JMenuBar;
-import javax.swing.JMenuItem;
-import javax.swing.JOptionPane;
-import javax.swing.JPanel;
-import javax.swing.JScrollPane;
-import javax.swing.JSeparator;
-import javax.swing.JSplitPane;
-import javax.swing.JTabbedPane;
-import javax.swing.JTextField;
-import javax.swing.KeyStroke;
-import javax.swing.ListSelectionModel;
-import javax.swing.ScrollPaneConstants;
-import javax.swing.SwingUtilities;
-import javax.swing.Timer;
-import javax.swing.ToolTipManager;
-import javax.swing.border.TitledBorder;
-import javax.swing.event.TreeSelectionEvent;
-import javax.swing.event.TreeSelectionListener;
-import javax.swing.filechooser.FileFilter;
-import javax.swing.tree.DefaultTreeSelectionModel;
-import javax.swing.tree.TreePath;
-import javax.swing.tree.TreeSelectionModel;
-
 import net.miginfocom.swing.MigLayout;
 import net.sf.openrocket.aerodynamics.WarningSet;
 import net.sf.openrocket.communication.UpdateInfo;
@@ -76,6 +19,7 @@ import net.sf.openrocket.gui.dialogs.ComponentAnalysisDialog;
 import net.sf.openrocket.gui.dialogs.ExampleDesignDialog;
 import net.sf.openrocket.gui.dialogs.LicenseDialog;
 import net.sf.openrocket.gui.dialogs.MotorDatabaseLoadingDialog;
+import net.sf.openrocket.gui.dialogs.PrintDialog;
 import net.sf.openrocket.gui.dialogs.SwingWorkerDialog;
 import net.sf.openrocket.gui.dialogs.UpdateInfoDialog;
 import net.sf.openrocket.gui.dialogs.WarningDialog;
@@ -97,6 +41,62 @@ import net.sf.openrocket.util.Reflection;
 import net.sf.openrocket.util.SaveFileWorker;
 import net.sf.openrocket.util.TestRockets;
 
+import javax.swing.Action;
+import javax.swing.InputMap;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JFileChooser;
+import javax.swing.JFrame;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSeparator;
+import javax.swing.JSplitPane;
+import javax.swing.JTabbedPane;
+import javax.swing.JTextField;
+import javax.swing.KeyStroke;
+import javax.swing.ListSelectionModel;
+import javax.swing.ScrollPaneConstants;
+import javax.swing.SwingUtilities;
+import javax.swing.Timer;
+import javax.swing.ToolTipManager;
+import javax.swing.border.TitledBorder;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.filechooser.FileFilter;
+import javax.swing.tree.DefaultTreeSelectionModel;
+import javax.swing.tree.TreePath;
+import javax.swing.tree.TreeSelectionModel;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Toolkit;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.InvocationTargetException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.concurrent.ExecutionException;
+
 public class BasicFrame extends JFrame {
        private static final LogHelper log = Application.getLogger();
        
@@ -480,7 +480,18 @@ public class BasicFrame extends JFrame {
                menu.add(item);
                
                menu.addSeparator();
-               
+
+
+        item = new JMenuItem("Print...", KeyEvent.VK_P);
+        item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, ActionEvent.CTRL_MASK));
+        item.getAccessibleContext().setAccessibleDescription("Print parts list and fin template");
+        item.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                printAction();
+            }
+        });
+        menu.add(item);
+        
                item = new JMenuItem("Quit", KeyEvent.VK_Q);
                item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.CTRL_MASK));
                item.getAccessibleContext().setAccessibleDescription("Quit the program");
@@ -1113,7 +1124,14 @@ public class BasicFrame extends JFrame {
                        closeAction();
                }
        }
-       
+
+    /**
+     * 
+     */
+    public void printAction() {
+        new PrintDialog(document);
+    }
+    
        /**
         * Open a new design window with a basic rocket+stage.
         */
diff --git a/src/net/sf/openrocket/gui/print/DesignReport.java b/src/net/sf/openrocket/gui/print/DesignReport.java
new file mode 100644 (file)
index 0000000..bdd8309
--- /dev/null
@@ -0,0 +1,457 @@
+/*
+ * DrawingsPrintable.java
+ */
+package net.sf.openrocket.gui.print;
+
+import com.itextpdf.text.Document;
+import com.itextpdf.text.DocumentException;
+import com.itextpdf.text.Element;
+import com.itextpdf.text.Paragraph;
+import com.itextpdf.text.Rectangle;
+import com.itextpdf.text.pdf.BaseFont;
+import com.itextpdf.text.pdf.DefaultFontMapper;
+import com.itextpdf.text.pdf.PdfContentByte;
+import com.itextpdf.text.pdf.PdfPCell;
+import com.itextpdf.text.pdf.PdfPTable;
+import com.itextpdf.text.pdf.PdfWriter;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.gui.figureelements.FigureElement;
+import net.sf.openrocket.gui.figureelements.RocketInfo;
+import net.sf.openrocket.gui.print.visitor.BaseVisitorStrategy;
+import net.sf.openrocket.gui.print.visitor.MotorMountVisitorStrategy;
+import net.sf.openrocket.gui.print.visitor.StageVisitorStrategy;
+import net.sf.openrocket.gui.scalefigure.RocketPanel;
+import net.sf.openrocket.motor.Motor;
+import net.sf.openrocket.rocketcomponent.ComponentVisitor;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.Prefs;
+
+import java.awt.Graphics2D;
+import java.io.IOException;
+import java.text.DecimalFormat;
+import java.util.List;
+
+/**
+ * <pre>
+ * #  Title # Section describing the rocket in general without motors
+ * # Section describing the rocket in general without motors
+ * <p/>
+ * design name
+ * empty mass & CG
+ * CP position
+ * CP position at 5 degree AOA (or similar)
+ * number of stages
+ * parachute/streamer sizes
+ * max. diameter (caliber)
+ * velocity at exit of rail/rod
+ * minimum safe velocity reached in x inches/cm
+ * <p/>
+ * # Section for each motor configuration
+ * <p/>
+ * a summary of the motors, e.g. 3xC6-0; B4-6
+ * a list of the motors including the manufacturer, designation (maybe also info like burn time, grams of propellant,
+ * total impulse)
+ * total grams of propellant
+ * total impulse
+ * takeoff weight
+ * CG and CP position, stability margin
+ * predicted flight altitude, max. velocity and max. acceleration
+ * predicted velocity at chute deployment
+ * predicted descent rate
+ * Thrust to Weight Ratio of each stage
+ * <p/>
+ * </pre>
+ */
+public class DesignReport extends BaseVisitorStrategy {
+
+    /**
+     * The OR Document.
+     */
+    private OpenRocketDocument rocketDocument;
+
+    /**
+     * A panel used for rendering of the design diagram.
+     */
+    final RocketPanel panel;
+
+    /**
+     * A stage visitor.
+     */
+    private StageVisitorStrategy svs = new StageVisitorStrategy();
+
+    /**
+     * Constructor.
+     *
+     * @param theRocDoc the OR document
+     * @param theIDoc   the iText document
+     */
+    public DesignReport (OpenRocketDocument theRocDoc, Document theIDoc) {
+        super(theIDoc, null);
+        rocketDocument = theRocDoc;
+        panel = new RocketPanel(rocketDocument);
+    }
+
+    /**
+     * Main entry point.  Prints the rocket drawing and design data.
+     *
+     * @param writer a direct byte writer
+     */
+    public void print (PdfWriter writer) {
+        if (writer == null) {
+            return;
+        }
+        com.itextpdf.text.Rectangle pageSize = document.getPageSize();
+        int pageImageableWidth = (int) pageSize.getWidth() - (int) pageSize.getBorderWidth() * 2;
+        int pageImageableHeight = (int) pageSize.getHeight() / 2 - (int) pageSize.getBorderWidthTop();
+
+        PrintUtilities.addText(document, PrintUtilities.BIG_BOLD, "Rocket Design");
+
+        Rocket rocket = rocketDocument.getRocket();
+        final Configuration configuration = rocket.getDefaultConfiguration();
+        configuration.setAllStages();
+        PdfContentByte canvas = writer.getDirectContent();
+
+        final PrintFigure figure = new PrintFigure(configuration);
+
+        FigureElement cp = panel.getExtraCP();
+        FigureElement cg = panel.getExtraCG();
+        RocketInfo text = panel.getExtraText();
+
+        double scale = paintRocketDiagram(pageImageableWidth, pageImageableHeight, canvas, figure, cp, cg);
+
+        canvas.beginText();
+        try {
+            canvas.setFontAndSize(BaseFont.createFont(PrintUtilities.NORMAL.getFamilyname(), BaseFont.CP1252,
+                                                      BaseFont.EMBEDDED), PrintUtilities.NORMAL_FONT_SIZE);
+        }
+        catch (DocumentException e) {
+            e.printStackTrace();
+        }
+        catch (IOException e) {
+            e.printStackTrace();
+        }
+        int figHeightPts = (int) (PrintUnit.METERS.toPoints(figure.getFigureHeight()) * 0.4 * (scale / PrintUnit.METERS
+                .toPoints(1)));
+        final int diagramHeight = pageImageableHeight * 2 - 70 - (int) (figHeightPts);
+        canvas.moveText(document.leftMargin() + pageSize.getBorderWidthLeft(), diagramHeight);
+        canvas.moveTextWithLeading(0, -16);
+
+        float initialY = canvas.getYTLM();
+
+        canvas.showText(rocketDocument.getRocket().getName());
+
+        canvas.newlineShowText("Stages: ");
+        canvas.showText("" + rocket.getStageCount());
+
+
+        if (configuration.hasMotors()) {
+            canvas.newlineShowText("Mass (with motor" + ((configuration.getStageCount() > 1) ? "s)" : ")"));
+        }
+        else {
+            canvas.newlineShowText("Mass (Empty): ");
+        }
+        canvas.showText(text.getMass(UnitGroup.UNITS_MASS.getDefaultUnit()));
+
+        canvas.newlineShowText("Stability: ");
+        canvas.showText(text.getStability());
+
+        canvas.newlineShowText("Cg: ");
+        canvas.showText(text.getCg());
+
+        canvas.newlineShowText("Cp: ");
+        canvas.showText(text.getCp());
+        canvas.endText();
+
+        try {
+            //Move the internal pointer of the document below that of what was just written using the direct byte buffer.
+            Paragraph paragraph = new Paragraph();
+            float finalY = canvas.getYTLM();
+            int heightOfDiagramAndText = (int) (pageSize.getHeight() - (finalY - initialY + diagramHeight));
+
+            paragraph.setSpacingAfter(heightOfDiagramAndText);
+            document.add(paragraph);
+
+            String[] mids = rocket.getMotorConfigurationIDs();
+
+            List<Double> stages = getStageWeights(rocket);
+
+
+            for (int j = 0; j < mids.length; j++) {
+                String mid = mids[j];
+                if (mid != null) {
+                    MotorMountVisitorStrategy mmvs = new MotorMountVisitorStrategy(document, mid);
+                    rocket.accept(new ComponentVisitor(mmvs));
+                    PdfPTable parent = new PdfPTable(2);
+                    parent.setWidthPercentage(100);
+                    parent.setHorizontalAlignment(Element.ALIGN_LEFT);
+                    parent.setSpacingBefore(0);
+                    parent.setWidths(new int[]{1, 3});
+                    int leading = 0;
+                    //The first motor config is always null.  Skip it and the top-most motor, then set the leading.
+                    if (j > 1) {
+                        leading = 25;
+                    }
+                    addFlightData(rocket, mid, parent, leading);
+                    addMotorData(mmvs.getMotors(), parent, stages);
+                    document.add(parent);
+                }
+            }
+        }
+        catch (DocumentException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Paint a diagram of the rocket into the PDF document.
+     *
+     * @param thePageImageableWidth  the number of points in the width of the page available for drawing
+     * @param thePageImageableHeight the number of points in the height of the page available for drawing
+     * @param theCanvas              the direct byte writer
+     * @param theFigure              the print figure
+     * @param theCp                  the center of pressure figure element
+     * @param theCg                  the center of gravity figure element
+     *
+     * @return the scale of the diagram
+     */
+    private double paintRocketDiagram (final int thePageImageableWidth, final int thePageImageableHeight, final PdfContentByte theCanvas, final PrintFigure theFigure, final FigureElement theCp, final FigureElement theCg) {
+        theFigure.clearAbsoluteExtra();
+        theFigure.clearRelativeExtra();
+        theFigure.addRelativeExtra(theCp);
+        theFigure.addRelativeExtra(theCg);
+        theFigure.updateFigure();
+
+        double scale =
+                (thePageImageableWidth * 2.2) / theFigure.getFigureWidth();
+
+        theFigure.setScale(scale);
+        /*
+         * page dimensions are in points-per-inch, which, in Java2D, are the same as pixels-per-inch; thus we don't need any conversion
+         */
+        theFigure.setSize(thePageImageableWidth, thePageImageableHeight);
+        theFigure.updateFigure();
+
+
+        final DefaultFontMapper mapper = new DefaultFontMapper();
+        Graphics2D g2d = theCanvas.createGraphics(thePageImageableWidth, thePageImageableHeight * 2, mapper);
+        g2d.translate(20, 120);
+
+        g2d.scale(0.4d, 0.4d);
+        theFigure.paint(g2d);
+        g2d.dispose();
+        return scale;
+    }
+
+    /**
+     * Add the motor data for a motor configuration to the table.
+     *
+     * @param motors a motor configuration's list of motors
+     * @param parent the parent to which the motor data will be added
+     */
+    private void addMotorData (List<Motor> motors, final PdfPTable parent, List<Double> weight) {
+
+        PdfPTable motorTable = new PdfPTable(7);
+        motorTable.setWidthPercentage(68);
+        motorTable.setHorizontalAlignment(Element.ALIGN_LEFT);
+
+        final PdfPCell motorCell = ITextHelper.createCell("Motor", PdfPCell.BOTTOM);
+        final int mPad = 12;
+        motorCell.setPaddingLeft(mPad);
+        motorTable.addCell(motorCell);
+        motorTable.addCell(ITextHelper.createCell("Avg Thrust", PdfPCell.BOTTOM));
+        motorTable.addCell(ITextHelper.createCell("Burn Time", PdfPCell.BOTTOM));
+        motorTable.addCell(ITextHelper.createCell("Max Thrust", PdfPCell.BOTTOM));
+        motorTable.addCell(ITextHelper.createCell("Total Impulse", PdfPCell.BOTTOM));
+        motorTable.addCell(ITextHelper.createCell("Thrust to Weight", PdfPCell.BOTTOM));
+        motorTable.addCell(ITextHelper.createCell("Dimensions", PdfPCell.BOTTOM));
+
+        DecimalFormat df = new DecimalFormat("#,##0.0#");
+        for (int i = 0; i < motors.size(); i++) {
+            int border = Rectangle.BOTTOM;
+            if (i == motors.size() - 1) {
+                border = Rectangle.NO_BORDER;
+            }
+            Motor motor = motors.get(i);
+            final PdfPCell motorVCell = ITextHelper.createCell(motor.getDesignation(), border);
+            motorVCell.setPaddingLeft(mPad);
+            motorTable.addCell(motorVCell);
+            motorTable.addCell(ITextHelper.createCell(df.format(motor.getAverageThrustEstimate()) + " " + UnitGroup
+                    .UNITS_FORCE
+                    .getDefaultUnit().toString(), border));
+            motorTable.addCell(ITextHelper.createCell(df.format(motor.getBurnTimeEstimate()) + " " + UnitGroup
+                    .UNITS_FLIGHT_TIME
+                    .getDefaultUnit().toString(), border));
+            motorTable.addCell(ITextHelper.createCell(df.format(motor.getMaxThrustEstimate()) + " " + UnitGroup
+                    .UNITS_FORCE.getDefaultUnit()
+                    .toString(), border));
+            motorTable.addCell(ITextHelper.createCell(df.format(motor.getTotalImpulseEstimate()) + " " + UnitGroup
+                    .UNITS_IMPULSE
+                    .getDefaultUnit().toString(), border));
+            double ttw = motor.getAverageThrustEstimate() / getStageWeight(weight, i);
+            motorTable.addCell(ITextHelper.createCell(df.format(ttw) + ":1", border));
+
+            final Unit motorUnit = UnitGroup.UNITS_MOTOR_DIMENSIONS
+                    .getDefaultUnit();
+            motorTable.addCell(ITextHelper.createCell(motorUnit.toString(motor.getDiameter()) +
+                                                      "/" +
+                                                      motorUnit.toString(motor.getLength()) + " " +
+                                                      motorUnit.toString(), border));
+        }
+        PdfPCell c = new PdfPCell(motorTable);
+        c.setBorder(PdfPCell.LEFT);
+        c.setBorderWidthTop(0f);
+        parent.addCell(c);
+    }
+
+
+    /**
+     * Add the motor data for a motor configuration to the table.
+     *
+     * @param theRocket the rocket
+     * @param mid       a motor configuration id
+     * @param parent    the parent to which the motor data will be added
+     * @param leading   the number of points for the leading
+     */
+    private void addFlightData (final Rocket theRocket, final String mid, final PdfPTable parent, int leading) {
+        FlightData flight = null;
+        if (theRocket.getMotorConfigurationIDs().length > 1) {
+            Rocket duplicate = theRocket.copy();
+            Simulation simulation = Prefs.getBackgroundSimulation(duplicate);
+            simulation.getConditions().setMotorConfigurationID(mid);
+
+            flight = PrintSimulationWorker.doit(simulation);
+
+            if (flight != null) {
+                try {
+                    final Unit distanceUnit = UnitGroup.UNITS_DISTANCE.getDefaultUnit();
+                    final Unit velocityUnit = UnitGroup.UNITS_VELOCITY.getDefaultUnit();
+                    final Unit flightUnit = UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit();
+
+                    PdfPTable labelTable = new PdfPTable(2);
+                    labelTable.setWidths(new int[]{3, 2});
+                    final Paragraph chunk = ITextHelper.createParagraph(stripBrackets(
+                            theRocket.getMotorConfigurationNameOrDescription(mid)), PrintUtilities.BOLD);
+                    chunk.setLeading(leading);
+                    chunk.setSpacingAfter(3f);
+
+                    document.add(chunk);
+
+                    DecimalFormat df = new DecimalFormat("#,##0.0#");
+
+                    final PdfPCell cell = ITextHelper.createCell("Altitude", 2, 2);
+                    cell.setUseBorderPadding(false);
+                    cell.setBorderWidthTop(0f);
+                    labelTable.addCell(cell);
+                    labelTable.addCell(ITextHelper.createCell(df.format(flight.getMaxAltitude()) + " " + distanceUnit,
+                                                              2, 2));
+
+                    labelTable.addCell(ITextHelper.createCell("Flight Time", 2, 2));
+                    labelTable.addCell(ITextHelper.createCell(df.format(flight.getFlightTime()) + " " + flightUnit, 2,
+                                                              2));
+
+                    labelTable.addCell(ITextHelper.createCell("Time to Apogee", 2, 2));
+                    labelTable.addCell(ITextHelper.createCell(df.format(flight.getTimeToApogee()) + " " + flightUnit, 2,
+                                                              2));
+
+                    labelTable.addCell(ITextHelper.createCell("Velocity off Pad", 2, 2));
+                    labelTable.addCell(ITextHelper.createCell(df.format(
+                            flight.getLaunchRodVelocity()) + " " + velocityUnit, 2, 2));
+
+                    labelTable.addCell(ITextHelper.createCell("Max Velocity", 2, 2));
+                    labelTable.addCell(ITextHelper.createCell(df.format(flight.getMaxVelocity()) + " " + velocityUnit,
+                                                              2, 2));
+
+                    labelTable.addCell(ITextHelper.createCell("Landing Velocity", 2, 2));
+                    labelTable.addCell(ITextHelper.createCell(df.format(
+                            flight.getGroundHitVelocity()) + " " + velocityUnit, 2, 2));
+
+                    //Add the table to the parent; have to wrap it in a cell
+                    PdfPCell c = new PdfPCell(labelTable);
+                    c.setBorder(PdfPCell.RIGHT);
+                    c.setBorderWidthTop(0);
+                    c.setTop(0);
+                    parent.addCell(c);
+                }
+                catch (DocumentException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    /**
+     * Strip [] brackets from a string.
+     *
+     * @param target the original string
+     *
+     * @return target with [] removed
+     */
+    private String stripBrackets (String target) {
+        return stripLeftBracket(stripRightBracket(target));
+    }
+
+    /**
+     * Strip [ from a string.
+     *
+     * @param target the original string
+     *
+     * @return target with [ removed
+     */
+    private String stripLeftBracket (String target) {
+        return target.replace("[", "");
+    }
+
+    /**
+     * Strip ] from a string.
+     *
+     * @param target the original string
+     *
+     * @return target with ] removed
+     */
+    private String stripRightBracket (String target) {
+        return target.replace("]", "");
+    }
+
+    /**
+     * Use a visitor to get the sorted list of Stage references, then from those get the stage masses and convert to
+     * weight.
+     *
+     * @param rocket the rocket
+     *
+     * @return a sorted list of Stage weights (mass * gravity), in Newtons
+     */
+    private List<Double> getStageWeights (Rocket rocket) {
+        rocket.accept(new ComponentVisitor(svs));
+        svs.close();
+        List<Double> stages = svs.getStages();
+        //TODO: Add in motor mass
+        for (int i = 0; i < stages.size(); i++) {
+            Double stage = stages.get(i);
+            stages.set(i, stage * 9.80665);
+        }
+        return stages;
+    }
+
+    /**
+     * Compute the total stage weight from a list of stage weights.  This sums up the weight of the given stage plus all
+     * stages that sit atop it (depend upon it for thrust).
+     *
+     * @param weights the list of stage weights, in Newtons
+     * @param stage   a stage number, 0 being topmost stage
+     *
+     * @return the total weight of the stage and all stages sitting atop the given stage, in Newtons
+     */
+    private double getStageWeight (List<Double> weights, int stage) {
+
+        double result = 0d;
+        for (int i = 0; i <= stage; i++) {
+            result += weights.get(i);
+        }
+        return result;
+    }
+}
diff --git a/src/net/sf/openrocket/gui/print/ITextHelper.java b/src/net/sf/openrocket/gui/print/ITextHelper.java
new file mode 100644 (file)
index 0000000..890c76e
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * ITextHelper.java
+ */
+package net.sf.openrocket.gui.print;
+
+import com.itextpdf.text.Chunk;
+import com.itextpdf.text.Font;
+import com.itextpdf.text.Paragraph;
+import com.itextpdf.text.Phrase;
+import com.itextpdf.text.Rectangle;
+import com.itextpdf.text.pdf.PdfPCell;
+import com.itextpdf.text.pdf.PdfPTable;
+
+/**
+ * A bunch of helper methods for creating iText components.
+ */
+public final class ITextHelper {
+
+    public static PdfPCell createCell () {
+        return createCell(Rectangle.BOTTOM);
+    }
+
+    public static PdfPCell createCell (int border) {
+        PdfPCell result = new PdfPCell();
+        result.setBorder(border);
+
+        return result;
+    }
+
+    public static PdfPCell createCell (PdfPTable table) {
+        PdfPCell result = new PdfPCell();
+        result.setBorder(PdfPCell.NO_BORDER);
+        result.addElement(table);
+
+        return result;
+    }
+
+    public static PdfPCell createCell (String v) {
+        return createCell(v, Rectangle.NO_BORDER);
+    }
+    
+    public static PdfPCell createCell (String v, Font font) {
+        return createCell(v, font);
+    }
+
+    public static PdfPCell createCell (String v, int leftPad, int rightPad) {
+        PdfPCell c = createCell(v, Rectangle.NO_BORDER);
+        c.setPaddingLeft(leftPad);
+        c.setPaddingRight(rightPad);
+        return c;
+    }
+
+    public static PdfPCell createCell (String v, int border) {
+        PdfPCell result = new PdfPCell();
+        result.setBorder(border);
+        Chunk c = new Chunk();
+        c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE));
+        c.append(v);
+        result.addElement(c);
+        return result;
+    }
+
+    public static Phrase createPhrase (String text, Font font) {
+
+        Phrase p = new Phrase();
+        final Chunk chunk = new Chunk(text);
+        chunk.setFont(font);
+        p.add(chunk);
+        return p;
+    }
+
+    public static Phrase createPhrase (String text) {
+        return createPhrase(text, PrintUtilities.NORMAL);
+    }
+
+    public static Paragraph createParagraph (String text, Font font) {
+
+        Paragraph p = new Paragraph();
+        final Chunk chunk = new Chunk(text);
+        chunk.setFont(font);
+        p.add(chunk);
+        return p;
+    }
+
+    public static Paragraph createParagraph (String text) {
+        return createParagraph(text, PrintUtilities.NORMAL);
+    }
+}
\ No newline at end of file
diff --git a/src/net/sf/openrocket/gui/print/OpenRocketPrintable.java b/src/net/sf/openrocket/gui/print/OpenRocketPrintable.java
new file mode 100644 (file)
index 0000000..58d5ad3
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * OpenRocketPrintable.java
+ */
+package net.sf.openrocket.gui.print;
+
+/**
+ * This enumeration identifies the various types of information that may be printed.
+ */
+public enum OpenRocketPrintable {
+    //PARTS_LIST("Parts list", true, 0),
+    PARTS_DETAIL("Parts detail", true, 1),
+    FIN_TEMPLATE("Fin templates", true, 2),
+    DESIGN_REPORT("Design Report", false, 3);
+
+    /**
+     * The description - will be displayed in the JTree.
+     */
+    private String description;
+
+    /**
+     * Flag that indicates if the enum value is different depending upon stage.
+     */
+    private boolean stageSpecific;
+
+    /**
+     * The order of the item as it appears in the printed document.
+     */
+    private int order;
+
+    /**
+     * Constructor.
+     *
+     * @param s      the displayable description
+     * @param staged indicates if the printable is stage dependent
+     * @param idx    the relative print order
+     */
+    OpenRocketPrintable (String s, boolean staged, int idx) {
+        description = s;
+        stageSpecific = staged;
+        order = idx;
+    }
+
+    /**
+     * Get the description of this printable.
+     *
+     * @return a displayable string
+     */
+    public String getDescription () {
+        return description;
+    }
+
+    /**
+     * Answers if this enum value has different meaning depending upon the stage.
+     *
+     * @return true if the printable is stage dependent
+     */
+    public boolean isStageSpecific () {
+        return stageSpecific;
+    }
+
+    /**
+     * Answer the print order.  This is relative to other enum values.  No two enum values will have the same print
+     * order value.
+     *
+     * @return a 0 based order (0 being first, or highest)
+     */
+    public int getPrintOrder () {
+        return order;
+    }
+
+    /**
+     * Look up an enum value based on the description.
+     *
+     * @param target the description
+     *
+     * @return an instance of this enum class or null if not found
+     */
+    public static OpenRocketPrintable findByDescription (String target) {
+        OpenRocketPrintable[] values = values();
+        for (OpenRocketPrintable value : values) {
+            if (value.getDescription().equalsIgnoreCase(target)) {
+                return value;
+            }
+        }
+        return null;
+    }
+}
diff --git a/src/net/sf/openrocket/gui/print/PDFPrintStreamDoc.java b/src/net/sf/openrocket/gui/print/PDFPrintStreamDoc.java
new file mode 100644 (file)
index 0000000..4b6a56b
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * PDFPrintStreamDoc.java
+ */
+package net.sf.openrocket.gui.print;
+
+import javax.print.Doc;
+import javax.print.DocFlavor;
+import javax.print.attribute.AttributeSetUtilities;
+import javax.print.attribute.DocAttributeSet;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+
+/**
+ */
+public class PDFPrintStreamDoc implements Doc {
+
+    private InputStream stream;
+    private DocAttributeSet attributeSet;
+
+    public PDFPrintStreamDoc (ByteArrayOutputStream ostream, DocAttributeSet attributes) {
+        stream = new ByteArrayInputStream(ostream.toByteArray());
+        if (attributes != null) {
+            attributeSet = AttributeSetUtilities.unmodifiableView(attributes);
+        }
+    }
+
+    public DocFlavor getDocFlavor () {
+        return DocFlavor.INPUT_STREAM.PDF;
+    }
+
+    public DocAttributeSet getAttributes () {
+        return attributeSet;
+    }
+
+    /* Since the data is to be supplied as an InputStream delegate to
+     * getStreamForBytes().
+     */
+
+    public Object getPrintData () throws IOException {
+        return getStreamForBytes();
+    }
+
+    public Reader getReaderForText () {
+        return null;
+    }
+
+    /* Return the print data as an InputStream.
+     * Always return the same instance.
+     */
+
+    public InputStream getStreamForBytes () throws IOException {
+        return stream;
+    }
+}
diff --git a/src/net/sf/openrocket/gui/print/PaperSize.java b/src/net/sf/openrocket/gui/print/PaperSize.java
new file mode 100644 (file)
index 0000000..e40d6f1
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * PaperSize.java
+ */
+package net.sf.openrocket.gui.print;
+
+import com.itextpdf.text.PageSize;
+import com.itextpdf.text.Rectangle;
+import com.itextpdf.text.RectangleReadOnly;
+
+import javax.print.attribute.standard.MediaSizeName;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Various mappings of paper sizes.
+ */
+public class PaperSize {
+    
+    private static Map<String, MediaSizeName> paperNames = new HashMap<String, MediaSizeName>();
+    private static Map<MediaSizeName, Rectangle> paperItext = new HashMap<MediaSizeName, Rectangle>();
+    
+    static {
+        populateNameMap();
+        populateITextSizeMap();
+    }
+    private PaperSize() {}
+    
+    public static MediaSizeName convert(String name) {
+        return paperNames.get(name);
+    }
+
+    public static Rectangle convert(MediaSizeName name) {
+        return paperItext.get(name);
+    }
+    
+    private static void populateNameMap() {
+        paperNames.put("iso-a0", MediaSizeName.ISO_A0);
+        paperNames.put("iso-a1", MediaSizeName.ISO_A1);
+        paperNames.put("iso-a2", MediaSizeName.ISO_A2);
+        paperNames.put("iso-a3", MediaSizeName.ISO_A3);
+        paperNames.put("iso-a4", MediaSizeName.ISO_A4);
+        paperNames.put("iso-a5", MediaSizeName.ISO_A5);
+        paperNames.put("iso-a6", MediaSizeName.ISO_A6);
+        paperNames.put("iso-a7", MediaSizeName.ISO_A7);
+        paperNames.put("iso-a8", MediaSizeName.ISO_A8);
+        paperNames.put("iso-a9", MediaSizeName.ISO_A9);
+        paperNames.put("iso-a10", MediaSizeName.ISO_A10);
+        paperNames.put("iso-b0", MediaSizeName.ISO_B0);
+        paperNames.put("iso-b1", MediaSizeName.ISO_B1);
+        paperNames.put("iso-b2", MediaSizeName.ISO_B2);
+        paperNames.put("iso-b3", MediaSizeName.ISO_B3);
+        paperNames.put("iso-b4", MediaSizeName.ISO_B4);
+        paperNames.put("iso-b5", MediaSizeName.ISO_B5);
+        paperNames.put("iso-b6", MediaSizeName.ISO_B6);
+        paperNames.put("iso-b7", MediaSizeName.ISO_B7);
+        paperNames.put("iso-b8", MediaSizeName.ISO_B8);
+        paperNames.put("iso-b9", MediaSizeName.ISO_B9);
+        paperNames.put("iso-b10", MediaSizeName.ISO_B10);
+        paperNames.put("na-letter", MediaSizeName.NA_LETTER);
+        paperNames.put("na-legal", MediaSizeName.NA_LEGAL);
+        paperNames.put("na-8x10", MediaSizeName.NA_8X10);
+        paperNames.put("na-5x7", MediaSizeName.NA_5X7);
+        paperNames.put("executive", MediaSizeName.EXECUTIVE);
+        paperNames.put("folio", MediaSizeName.FOLIO);
+        paperNames.put("invoice", MediaSizeName.INVOICE);
+        paperNames.put("tabloid", MediaSizeName.TABLOID);
+        paperNames.put("ledger", MediaSizeName.LEDGER);
+        paperNames.put("quarto", MediaSizeName.QUARTO);
+        paperNames.put("iso-c0", MediaSizeName.ISO_C0);
+        paperNames.put("iso-c1", MediaSizeName.ISO_C1);
+        paperNames.put("iso-c2", MediaSizeName.ISO_C2);
+        paperNames.put("iso-c3", MediaSizeName.ISO_C3);
+        paperNames.put("iso-c4", MediaSizeName.ISO_C4);
+        paperNames.put("iso-c5", MediaSizeName.ISO_C5);
+        paperNames.put("iso-c6", MediaSizeName.ISO_C6);
+        paperNames.put("iso-designated-long", MediaSizeName.ISO_DESIGNATED_LONG);
+        paperNames.put("jis-b0", MediaSizeName.JIS_B0);
+        paperNames.put("jis-b1", MediaSizeName.JIS_B1);
+        paperNames.put("jis-b2", MediaSizeName.JIS_B2);
+        paperNames.put("jis-b3", MediaSizeName.JIS_B3);
+        paperNames.put("jis-b4", MediaSizeName.JIS_B4);
+        paperNames.put("jis-b5", MediaSizeName.JIS_B5);
+        paperNames.put("jis-b6", MediaSizeName.JIS_B6);
+        paperNames.put("jis-b7", MediaSizeName.JIS_B7);
+        paperNames.put("jis-b8", MediaSizeName.JIS_B8);
+        paperNames.put("jis-b9", MediaSizeName.JIS_B9);
+        paperNames.put("jis-b10", MediaSizeName.JIS_B10);
+        paperNames.put("a", MediaSizeName.A);
+        paperNames.put("b", MediaSizeName.B);
+        paperNames.put("c", MediaSizeName.C);
+        paperNames.put("d", MediaSizeName.D);
+        paperNames.put("e", MediaSizeName.E);
+    }
+
+    private static void populateITextSizeMap() {
+        paperItext.put(MediaSizeName.ISO_A0, PageSize.A0);
+        paperItext.put(MediaSizeName.ISO_A1, PageSize.A1);
+        paperItext.put(MediaSizeName.ISO_A2, PageSize.A2);
+        paperItext.put(MediaSizeName.ISO_A3, PageSize.A3);
+        paperItext.put(MediaSizeName.ISO_A4, PageSize.A4);
+        paperItext.put(MediaSizeName.ISO_A5, PageSize.A5);
+        paperItext.put(MediaSizeName.ISO_A6, PageSize.A6);
+        paperItext.put(MediaSizeName.ISO_A7, PageSize.A7);
+        paperItext.put(MediaSizeName.ISO_A8, PageSize.A8);
+        paperItext.put(MediaSizeName.ISO_A9, PageSize.A9);
+        paperItext.put(MediaSizeName.ISO_A10, PageSize.A10);
+        paperItext.put(MediaSizeName.ISO_B0, PageSize.B0);
+        paperItext.put(MediaSizeName.ISO_B1, PageSize.B1);
+        paperItext.put(MediaSizeName.ISO_B2, PageSize.B2);
+        paperItext.put(MediaSizeName.ISO_B3, PageSize.B3);
+        paperItext.put(MediaSizeName.ISO_B4, PageSize.B4);
+        paperItext.put(MediaSizeName.ISO_B5, PageSize.B5);
+        paperItext.put(MediaSizeName.ISO_B6, PageSize.B6);
+        paperItext.put(MediaSizeName.ISO_B7, PageSize.B7);
+        paperItext.put(MediaSizeName.ISO_B8, PageSize.B8);
+        paperItext.put(MediaSizeName.ISO_B9, PageSize.B9);
+        paperItext.put(MediaSizeName.ISO_B10, PageSize.B10);
+        paperItext.put(MediaSizeName.NA_LETTER, PageSize.LETTER);
+        paperItext.put(MediaSizeName.NA_LEGAL, PageSize.LEGAL);
+        paperItext.put(MediaSizeName.EXECUTIVE, PageSize.EXECUTIVE);
+        paperItext.put(MediaSizeName.A, PageSize.LETTER);
+        paperItext.put(MediaSizeName.B, PageSize._11X17);
+        paperItext.put(MediaSizeName.C, new RectangleReadOnly(PrintUnit.INCHES.toPoints(17), PrintUnit.INCHES.toPoints(22)));
+        paperItext.put(MediaSizeName.D, new RectangleReadOnly(PrintUnit.INCHES.toPoints(22), PrintUnit.INCHES.toPoints(34)));
+        paperItext.put(MediaSizeName.E, new RectangleReadOnly(PrintUnit.INCHES.toPoints(34), PrintUnit.INCHES.toPoints(44)));
+    }
+    
+}
diff --git a/src/net/sf/openrocket/gui/print/PrintController.java b/src/net/sf/openrocket/gui/print/PrintController.java
new file mode 100644 (file)
index 0000000..41d4ffa
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * PrintController.java
+ *
+ */
+package net.sf.openrocket.gui.print;
+
+import com.itextpdf.text.Document;
+import com.itextpdf.text.DocumentException;
+import com.itextpdf.text.ExceptionConverter;
+import com.itextpdf.text.Rectangle;
+import com.itextpdf.text.pdf.PdfBoolean;
+import com.itextpdf.text.pdf.PdfName;
+import com.itextpdf.text.pdf.PdfWriter;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.gui.print.visitor.FinSetVisitorStrategy;
+import net.sf.openrocket.gui.print.visitor.PartsDetailVisitorStrategy;
+import net.sf.openrocket.rocketcomponent.ComponentVisitor;
+
+import javax.print.attribute.standard.MediaSizeName;
+import java.io.OutputStream;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * This is the main active object for printing.  It performs all actions necessary to create and populate the print file.
+ */
+public class PrintController {
+
+    /**
+     * Print the selected components to a PDF document.
+     * 
+     * @param doc          the OR document
+     * @param toBePrinted  the user chosen items to print
+     * @param outputFile   the file being written to
+     * @param msn          the paper size
+     */
+    public void print (OpenRocketDocument doc, Iterator<PrintableContext> toBePrinted, OutputStream outputFile, MediaSizeName msn) {
+
+        Document idoc = new Document(convertWithDefault(msn));
+        PdfWriter writer = null;
+//        OutputStream fileOutputStream = null;
+        try {
+//            fileOutputStream = outputFile;
+            writer = PdfWriter.getInstance(idoc, outputFile);
+            writer.setStrictImageSequence(true);
+
+            writer.addViewerPreference(PdfName.PRINTSCALING, PdfName.NONE);
+            writer.addViewerPreference(PdfName.PICKTRAYBYPDFSIZE, PdfBoolean.PDFTRUE);
+            try {
+                idoc.open();
+                Thread.sleep(1000);
+            }
+            catch (InterruptedException e) {
+            }
+            while (toBePrinted.hasNext()) {
+                PrintableContext printableContext = toBePrinted.next();
+
+                Set<Integer> stages = printableContext.getStageNumber();
+
+                switch (printableContext.getPrintable()) {
+                    case DESIGN_REPORT:
+                        DesignReport dp = new DesignReport(doc, idoc);
+                        dp.print(writer);
+                        idoc.newPage();
+                        break;
+                    case FIN_TEMPLATE:
+                        final ComponentVisitor finVisitor = new ComponentVisitor(new FinSetVisitorStrategy(idoc,
+                                                                                                           writer,
+                                                                                                           stages));
+                        finVisitor.visit(doc.getRocket());
+                        finVisitor.close();
+                        break;
+                    case PARTS_DETAIL:
+                        final ComponentVisitor detailVisitor = new ComponentVisitor(new PartsDetailVisitorStrategy(idoc,
+                                                                                                                   writer,
+                                                                                                                   stages));
+                        detailVisitor.visit(doc.getRocket());
+                        detailVisitor.close();
+                        idoc.newPage();
+                        break;
+               /*     case PARTS_LIST:
+                        final ComponentVisitor partsVisitor = new ComponentVisitor(new PartsListVisitorStrategy(idoc,
+                                                                                                                writer,
+                                                                                                                stages));
+                        partsVisitor.visit(doc.getRocket());
+                        partsVisitor.close();
+                        idoc.newPage();
+                        break;
+                */}
+            }
+            //Stupid iText throws a really nasty exception if there is no data when close is called.
+            if (writer.getCurrentDocumentSize() <= 140) {
+                writer.setPageEmpty(false);
+            }
+            writer.close();
+            idoc.close();
+        }
+        catch (DocumentException e) {
+        }
+        catch (ExceptionConverter ec) {
+        }
+/*        finally {
+            if (fileOutputStream != null) {
+                try {
+                    fileOutputStream.close();
+                }
+                catch (IOException e) {
+                }
+            }
+        }
+  */  }
+
+    private Rectangle convertWithDefault (final MediaSizeName msn) {
+        Rectangle result = PaperSize.convert(msn);
+        if (result == null) {
+            result = PaperSize.convert(PrintUtilities.getDefaultMedia().getMediaSizeName());
+        }
+        return result;
+    }
+
+}
diff --git a/src/net/sf/openrocket/gui/print/PrintFigure.java b/src/net/sf/openrocket/gui/print/PrintFigure.java
new file mode 100644 (file)
index 0000000..ae90926
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * PrintFigure.java
+ */
+package net.sf.openrocket.gui.print;
+
+import net.sf.openrocket.gui.scalefigure.RocketFigure;
+import net.sf.openrocket.rocketcomponent.Configuration;
+
+/**
+ * A figure used to override the scale factor in RocketFigure.  This allows pinpoint scaling to allow a diagram
+ * to fit in the width of the chosen page size.
+ */
+public class PrintFigure extends RocketFigure {
+
+    /**
+     * Constructor.
+     * 
+     * @param configuration  the configuration
+     */
+    public PrintFigure (final Configuration configuration) {
+        super(configuration);
+    }
+
+    protected double computeTy (int heightPx) {
+        super.computeTy(heightPx);
+        return 0;
+    }
+    
+    public void setScale (final double theScale) {
+        this.scale = theScale; //dpi/0.0254*scaling;
+        updateFigure();
+    }
+}
diff --git a/src/net/sf/openrocket/gui/print/PrintSimulationWorker.java b/src/net/sf/openrocket/gui/print/PrintSimulationWorker.java
new file mode 100644 (file)
index 0000000..b0b31b3
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * PrintSimulationWorker.java
+ */
+package net.sf.openrocket.gui.print;
+
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.gui.main.SimulationWorker;
+import net.sf.openrocket.simulation.FlightData;
+
+/**
+ * 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.
+ */
+public class PrintSimulationWorker {
+
+    public static FlightData doit (Simulation sim) {
+        return new InnerPrintSimulationWorker(sim).doit();
+    }
+
+    static class InnerPrintSimulationWorker extends SimulationWorker {
+
+        public InnerPrintSimulationWorker (Simulation sim) {
+            super(sim);
+        }
+
+        public FlightData doit() {
+            return doInBackground();
+        }
+        @Override
+        protected void simulationDone () {
+            // Do nothing if cancelled
+            if (isCancelled()) {
+                return;
+            }
+
+            simulation.getSimulatedData();
+        }
+
+        
+        /**
+         * Called if the simulation is interrupted due to an exception.
+         *
+         * @param t the Throwable that caused the interruption
+         */
+        @Override
+        protected void simulationInterrupted (final Throwable t) {
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/net/sf/openrocket/gui/print/PrintUnit.java b/src/net/sf/openrocket/gui/print/PrintUnit.java
new file mode 100644 (file)
index 0000000..c603f2f
--- /dev/null
@@ -0,0 +1,158 @@
+/*
+ * PrintUnit.java
+ */
+package net.sf.openrocket.gui.print;
+
+/**
+ * Utilities for print units.
+ */
+public enum PrintUnit {
+    INCHES {
+        public double toInches(double d) { return d; }
+        public double toMillis(double d) { return d/INCHES_PER_MM; }
+        public double toCentis(double d) { return d/(INCHES_PER_MM*TEN); }
+        public double toMeters(double d) { return d/(INCHES_PER_MM*TEN*TEN*TEN); }
+        public long   toPoints(double d) { return (long)(d * POINTS_PER_INCH); }
+        public double convert(double d, PrintUnit u) { return u.toInches(d); }
+    },
+    MILLIMETERS {
+        public double toInches(double d) { return d * INCHES_PER_MM; }
+        public double toMillis(double d) { return d; }
+        public double toCentis(double d) { return d/TEN; }
+        public double toMeters(double d) { return d/(TEN*TEN*TEN); }
+        public long   toPoints(double d) { return INCHES.toPoints(toInches(d)); }
+        public double convert(double d, PrintUnit u) { return u.toMillis(d); }
+    },
+    CENTIMETERS {
+        public double toInches(double d) { return d * INCHES_PER_MM * TEN; }
+        public double toMillis(double d) { return d * TEN; }
+        public double toCentis(double d) { return d; }
+        public double toMeters(double d) { return d/(TEN*TEN); }
+        public long   toPoints(double d) { return INCHES.toPoints(toInches(d)); }
+        public double convert(double d, PrintUnit u) { return u.toCentis(d); }
+    },
+    METERS {
+        public double toInches(double d) { return d * INCHES_PER_MM * TEN * TEN * TEN; }
+        public double toMillis(double d) { return d * TEN * TEN * TEN; }
+        public double toCentis(double d) { return d * TEN * TEN; }
+        public double toMeters(double d) { return d; }
+        public long   toPoints(double d) { return INCHES.toPoints(toInches(d)); }
+        public double convert(double d, PrintUnit u) { return u.toMeters(d); }
+    },
+    POINTS {
+        public double toInches(double d) { return d/POINTS_PER_INCH; }
+        public double toMillis(double d) { return d/(POINTS_PER_INCH * INCHES_PER_MM); }
+        public double toCentis(double d) { return toMillis(d)/TEN; }
+        public double toMeters(double d) { return toMillis(d)/(TEN*TEN*TEN); }
+        public long   toPoints(double d) { return (long)d; }
+        public double convert(double d, PrintUnit u) { return u.toPoints(d); }
+    };
+
+    // Handy constants for conversion methods
+    public static final double INCHES_PER_MM = 0.0393700787d;
+    public static final double MM_PER_INCH = 1.0d/INCHES_PER_MM;
+    public static final long TEN = 10;
+    /**
+     * PPI is Postscript Point and is a standard of 72.  Java2D also uses this internally as a pixel-per-inch, so pixels
+     * and points are for the most part interchangeable (unless the defaults are changed), which makes translating
+     * between the screen and a print job easier.
+     * 
+     * Not to be confused with Dots-Per-Inch, which is printer and print mode dependent.
+     */
+    public static final int POINTS_PER_INCH = 72;
+
+    // To maintain full signature compatibility with 1.5, and to improve the
+    // clarity of the generated javadoc (see 6287639: Abstract methods in
+    // enum classes should not be listed as abstract), method convert
+    // etc. are not declared abstract but otherwise act as abstract methods.
+
+    /**
+     * Convert the given length in the given unit to this
+     * unit.  Conversions from finer to coarser granularities
+     * truncate, so may lose precision.
+     *
+     * <p>For example, to convert 10 inches to point, use:
+     * <tt>PrintUnit.POINTS.convert(10L, PrintUnit.INCHES)</tt>
+     *
+     * @param sourceLength the length in the given <tt>sourceUnit</tt>
+     * @param sourceUnit the unit of the <tt>sourceDuration</tt> argument
+     *
+     * @return the converted length in this unit,
+     * or <tt>Long.MIN_VALUE</tt> if conversion would negatively
+     * overflow, or <tt>Long.MAX_VALUE</tt> if it would positively overflow.
+     */
+    public double convert(double sourceLength, PrintUnit sourceUnit) {
+        throw new AbstractMethodError();
+    }
+
+    /**
+     * Equivalent to <tt>INCHES.convert(length, this)</tt>.
+     *
+     * @param length  the length
+     *
+     * @return the converted length,
+     * or <tt>Long.MIN_VALUE</tt> if conversion would negatively
+     * overflow, or <tt>Long.MAX_VALUE</tt> if it would positively overflow.
+     * @see #convert
+     */
+    public double toInches(double length) {
+        throw new AbstractMethodError();
+    }
+
+    /**
+     * Equivalent to <tt>MILLIMETERS.convert(length, this)</tt>.
+     *
+     * @param length  the length
+     *
+     * @return the converted length,
+     * or <tt>Long.MIN_VALUE</tt> if conversion would negatively
+     * overflow, or <tt>Long.MAX_VALUE</tt> if it would positively overflow.
+     * @see #convert
+     */
+    public double toMillis(double length) {
+        throw new AbstractMethodError();
+    }
+
+    /**
+     * Equivalent to <tt>CENTIMETERS.convert(length, this)</tt>.
+     *
+     * @param length  the length
+     *
+     * @return the converted length,
+     * or <tt>Long.MIN_VALUE</tt> if conversion would negatively
+     * overflow, or <tt>Long.MAX_VALUE</tt> if it would positively overflow.
+     * @see #convert
+     */
+    public double toCentis(double length) {
+        throw new AbstractMethodError();
+    }
+
+    /**
+     * Equivalent to <tt>METERS.convert(length, this)</tt>.
+     *
+     * @param length  the length
+     *
+     * @return the converted length,
+     * or <tt>Long.MIN_VALUE</tt> if conversion would negatively
+     * overflow, or <tt>Long.MAX_VALUE</tt> if it would positively overflow.
+     * @see #convert
+     */
+    public double toMeters(double length) {
+        throw new AbstractMethodError();
+    }
+
+    /**
+     * Equivalent to <tt>POINTS.convert(length, this)</tt>.
+     *
+     * @param length  the length
+     *
+     * @return the converted length,
+     * or <tt>Long.MIN_VALUE</tt> if conversion would negatively
+     * overflow, or <tt>Long.MAX_VALUE</tt> if it would positively overflow.
+     * @see #convert
+     */
+    public long toPoints(double length) {
+        throw new AbstractMethodError();
+    }
+
+}
\ No newline at end of file
diff --git a/src/net/sf/openrocket/gui/print/PrintUtilities.java b/src/net/sf/openrocket/gui/print/PrintUtilities.java
new file mode 100644 (file)
index 0000000..b2bc5c5
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+ * PrintUtilities.java
+ */
+package net.sf.openrocket.gui.print;
+
+
+import com.itextpdf.text.Chunk;
+import com.itextpdf.text.Document;
+import com.itextpdf.text.DocumentException;
+import com.itextpdf.text.Font;
+import com.itextpdf.text.Paragraph;
+
+import javax.print.DocFlavor;
+import javax.print.PrintService;
+import javax.print.PrintServiceLookup;
+import javax.print.ServiceUI;
+import javax.print.attribute.HashPrintRequestAttributeSet;
+import javax.print.attribute.PrintRequestAttributeSet;
+import javax.print.attribute.standard.MediaSize;
+import javax.swing.RepaintManager;
+import java.awt.Component;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.print.PageFormat;
+import java.awt.print.Printable;
+import java.awt.print.PrinterException;
+import java.awt.print.PrinterJob;
+import java.util.Locale;
+
+public class PrintUtilities implements Printable {
+
+    public static final int NORMAL_FONT_SIZE = Font.DEFAULTSIZE - 3;
+    public static final int SMALL_FONT_SIZE = NORMAL_FONT_SIZE - 3;
+
+    public static final Font BOLD = new Font(Font.FontFamily.HELVETICA, NORMAL_FONT_SIZE, Font.BOLD);
+    public static final Font BIG_BOLD = new Font(Font.FontFamily.HELVETICA, NORMAL_FONT_SIZE + 3, Font.BOLD);
+    public static final Font BOLD_UNDERLINED = new Font(Font.FontFamily.HELVETICA, NORMAL_FONT_SIZE,
+                                                        Font.BOLD | Font.UNDERLINE);
+    public static final Font NORMAL = new Font(Font.FontFamily.HELVETICA, NORMAL_FONT_SIZE);
+    public static final Font SMALL = new Font(Font.FontFamily.HELVETICA, SMALL_FONT_SIZE);
+
+
+    private Component componentToBePrinted;
+
+    public static void printComponent (Component c) {
+        new PrintUtilities(c).print();
+    }
+
+    public PrintUtilities (Component componentToBePrinted) {
+        this.componentToBePrinted = componentToBePrinted;
+    }
+
+    public void print () {
+        PrinterJob printJob = PrinterJob.getPrinterJob();
+        printJob.setPrintable(this);
+        if (printJob.printDialog()) {
+            try {
+                printJob.print();
+            }
+            catch (PrinterException pe) {
+                System.out.println("Error printing: " + pe);
+            }
+        }
+    }
+
+    public int print (Graphics g, PageFormat pageFormat, int pageIndex) {
+        if (pageIndex > 0) {
+            return (NO_SUCH_PAGE);
+        }
+        else {
+            Graphics2D g2d = (Graphics2D) g;
+            translateToJavaOrigin(g2d, pageFormat);
+            disableDoubleBuffering(componentToBePrinted);
+            componentToBePrinted.paint(g2d);
+            enableDoubleBuffering(componentToBePrinted);
+            return (PAGE_EXISTS);
+        }
+    }
+
+    public static void disableDoubleBuffering (Component c) {
+        RepaintManager currentManager = RepaintManager.currentManager(c);
+        currentManager.setDoubleBufferingEnabled(false);
+    }
+
+    public static void enableDoubleBuffering (Component c) {
+        RepaintManager currentManager = RepaintManager.currentManager(c);
+        currentManager.setDoubleBufferingEnabled(true);
+    }
+
+    public static PrintService askUserForPDFPrintService () {
+        return askUserForPrintService(DocFlavor.INPUT_STREAM.PDF);
+    }
+
+    public static PrintService askUserForPrintService (DocFlavor flavor) {
+        PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
+        PrintService svc = PrintServiceLookup.lookupDefaultPrintService();
+        PrintRequestAttributeSet attrs = new HashPrintRequestAttributeSet();
+        attrs.add(getDefaultMedia().getMediaSizeName());
+
+        return ServiceUI.printDialog(null, 100, 100, services, svc, flavor, attrs);
+    }
+
+    /**
+     * Sets the paper size for pages using these attributes to the default size for the default locale. The default size
+     * for locales in the United States and Canada is MediaType.NA_LETTER. The default size for all other locales is
+     * MediaType.ISO_A4.
+     */
+    public static MediaSize getDefaultMedia () {
+        String defaultCountry = Locale.getDefault().getCountry();
+        if (defaultCountry != null &&
+            (defaultCountry.equals(Locale.US.getCountry()) ||
+             defaultCountry.equals(Locale.CANADA.getCountry()))) {
+            return MediaSize.NA.LETTER;
+        }
+        else {
+            return MediaSize.ISO.A4;
+        }
+    }
+
+    /**
+     * Translate the page format coordinates onto the graphics object using Java's origin (top left).
+     *
+     * @param g2d        the graphics object
+     * @param pageFormat the print page format
+     */
+    public static void translateToJavaOrigin (Graphics2D g2d, PageFormat pageFormat) {
+        g2d.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
+    }
+
+    public static void addText (Document document, com.itextpdf.text.Font font, String title) {
+        Chunk sectionHeader = new Chunk(title);
+        sectionHeader.setFont(font);
+        try {
+            Paragraph p = new Paragraph();
+            p.add(sectionHeader);
+            document.add(p);
+        }
+        catch (DocumentException e) {
+            e.printStackTrace();
+        }
+    }
+}
diff --git a/src/net/sf/openrocket/gui/print/PrintableContext.java b/src/net/sf/openrocket/gui/print/PrintableContext.java
new file mode 100644 (file)
index 0000000..6645b69
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * PrintableContext.java
+ *
+ */
+package net.sf.openrocket.gui.print;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/**
+ * Instances of this class are meant to keep track of what the user has selected to be printed.
+ */
+public class PrintableContext implements Comparable<PrintableContext>, Iterable<PrintableContext> {
+
+    /**
+     * The stage number.  May be null for printables that have no stage meaning.
+     */
+    private Set<Integer> stageNumber;
+
+    /**
+     * The type of thing to be printed.
+     */
+    private OpenRocketPrintable printable;
+
+    private final Map<OpenRocketPrintable, Set<Integer>> previous = new TreeMap<OpenRocketPrintable, Set<Integer>>();
+
+    
+    public PrintableContext () {
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param theStageNumber the stage number of the printable; may be null if not applicable
+     * @param thePrintable   the type of the thing to be printed
+     *
+     * @throws IllegalArgumentException thrown if thePrintable.isStageSpecific
+     */
+    private PrintableContext (final Set<Integer> theStageNumber, final OpenRocketPrintable thePrintable)
+            throws IllegalArgumentException {
+        if (thePrintable.isStageSpecific() && theStageNumber == null) {
+            throw new IllegalArgumentException("A stage number must be provided when a printable is stage specific.");
+        }
+        stageNumber = theStageNumber;
+        printable = thePrintable;
+    }
+
+    public void add (final Integer theStageNumber, final OpenRocketPrintable thePrintable)
+            throws IllegalArgumentException {
+        Set<Integer> stages = previous.get(thePrintable);
+        if (stages == null) {
+            stages = new TreeSet<Integer>();
+            previous.put(thePrintable, stages);
+        }
+        if (theStageNumber != null) {
+            stages.add(theStageNumber);
+        }
+    }
+
+
+    public Iterator<PrintableContext> iterator () {
+        return new Iterator<PrintableContext>() {
+
+            Iterator<OpenRocketPrintable> keyIter = previous.keySet().iterator();
+
+            @Override
+            public boolean hasNext () {
+                return keyIter.hasNext();
+            }
+
+            @Override
+            public PrintableContext next () {
+                final OpenRocketPrintable key = keyIter.next();
+                return new PrintableContext(previous.get(key), key);
+            }
+
+            @Override
+            public void remove () {
+            }
+        };
+
+    }
+
+    /**
+     * Get the stage number, if it's applicable to the printable.
+     *
+     * @return the stage number
+     */
+    public Set<Integer> getStageNumber () {
+        return stageNumber;
+    }
+
+    /**
+     * Get the printable.
+     *
+     * @return the printable
+     */
+    public OpenRocketPrintable getPrintable () {
+        return printable;
+    }
+
+    @Override
+    public int compareTo (final PrintableContext other) {
+        return this.printable.getPrintOrder() - other.printable.getPrintOrder();
+    }
+
+}
diff --git a/src/net/sf/openrocket/gui/print/PrintableFinSet.java b/src/net/sf/openrocket/gui/print/PrintableFinSet.java
new file mode 100644 (file)
index 0000000..6f82423
--- /dev/null
@@ -0,0 +1,158 @@
+/*
+ * PrintableFinSet.java
+ */
+package net.sf.openrocket.gui.print;
+
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.util.Coordinate;
+
+import javax.swing.JPanel;
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.geom.GeneralPath;
+import java.awt.image.BufferedImage;
+import java.awt.print.PageFormat;
+import java.awt.print.Printable;
+import java.awt.print.PrinterException;
+
+/**
+ * This class allows for a FinSet to be printable.  It does so by decorating an existing finset (which will not be
+ * modified) and rendering it within a JPanel.  The JPanel is not actually visualized on a display, but instead renders
+ * it to a print device.
+ */
+public class PrintableFinSet extends JPanel implements Printable {
+
+    /**
+     * The actual fin set being printed.
+     */
+    private FinSet finset;
+
+    /**
+     * The object that represents the shape (outline) of the fin.  This gets drawn onto the Swing component.
+     */
+    private GeneralPath polygon = null;
+
+    /**
+     * The margin.
+     */
+    private final int margin = 20;
+    
+    /**
+     * Constructor.
+     *
+     * @param fs the finset to print
+     */
+    public PrintableFinSet (FinSet fs) {
+        super(false);
+        finset = fs;
+        init();
+        setBackground(Color.white);
+    }
+
+    /**
+     * Initialize the fin set polygon and set the size of the component.
+     */
+    private void init () {
+        Coordinate[] points = finset.getFinPointsWithTab();
+
+        polygon = new GeneralPath(GeneralPath.WIND_EVEN_ODD, points.length);
+        polygon.moveTo(0, 0);
+        
+        int minX = 0;
+        int minY = 0;
+        int maxX = 0;
+        int maxY = 0;
+
+        for (Coordinate point : points) {
+            final long x = PrintUnit.METERS.toPoints(point.x);
+            final long y = PrintUnit.METERS.toPoints(point.y);
+            minX = (int) Math.min(x, minX);
+            minY = (int) Math.min(y, minY);
+            maxX = (int) Math.max(x, maxX);
+            maxY = (int) Math.max(y, maxY);
+            polygon.lineTo(x, y);
+        }
+        polygon.closePath();
+        
+        setSize(maxX-minX+ margin, maxY-minY+ margin);
+    }
+
+    /**
+     * From the java.awt.print.Printable interface.
+     * 
+     * Prints the page at the specified index into the specified {@link java.awt.Graphics} context in the specified
+     * format. A <code>PrinterJob</code> calls the <code>Printable</code> interface to request that a page be rendered
+     * into the context specified by <code>graphics</code>.  The format of the page to be drawn is specified by
+     * <code>pageFormat</code>.  The zero based index of the requested page is specified by <code>pageIndex</code>. If
+     * the requested page does not exist then this method returns NO_SUCH_PAGE; otherwise PAGE_EXISTS is returned. The
+     * <code>Graphics</code> class or subclass implements the {@link java.awt.print.PrinterGraphics} interface to
+     * provide additional information.  If the <code>Printable</code> object aborts the print job then it throws a
+     * {@link java.awt.print.PrinterException}.
+     *
+     * @param graphics   the context into which the page is drawn
+     * @param pageFormat the size and orientation of the page being drawn
+     * @param pageIndex  the zero based index of the page to be drawn
+     *
+     * @return PAGE_EXISTS if the page is rendered successfully or NO_SUCH_PAGE if <code>pageIndex</code> specifies a
+     *         non-existent page.
+     *
+     * @throws java.awt.print.PrinterException
+     *          thrown when the print job is terminated.
+     */
+    @Override
+    public int print (final Graphics graphics, final PageFormat pageFormat, final int pageIndex)
+            throws PrinterException {
+
+        Graphics2D g2d = (Graphics2D) graphics;
+        PrintUtilities.translateToJavaOrigin(g2d, pageFormat);
+        PrintUtilities.disableDoubleBuffering(this);
+        paint(g2d);
+        PrintUtilities.enableDoubleBuffering(this);
+        return Printable.PAGE_EXISTS;
+    }
+
+    /**
+     * 
+     * Returns a generated image of the fin set.  May then be used wherever AWT images can be used, or
+     * converted to another image/picture format and used accordingly.
+     * 
+     * @return an awt image of the fin set
+     */
+    public Image createImage () {
+        int width = getWidth();
+        int height = getHeight();
+        // Create a buffered image in which to draw 
+        BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+        // Create a graphics contents on the buffered image 
+        Graphics2D g2d = bufferedImage.createGraphics();
+        // Draw graphics 
+        g2d.setBackground(Color.white);
+        g2d.clearRect(0, 0, width, height);
+        paintComponent(g2d);
+        // Graphics context no longer needed so dispose it 
+        g2d.dispose();
+        return bufferedImage;
+    }
+
+    /**
+     * Render the fin set onto the graphics context.  This is done by creating a GeneralPath component that follows the
+     * outline of the fin set coordinates to create a polygon, which is then drawn onto the graphics context.
+     * Through-the-wall fin tabs are supported if they are present.
+     *
+     * @param g the Java2D graphics context
+     */
+    @Override
+    public void paintComponent (Graphics g) {
+        super.paintComponent(g);
+        Graphics2D g2d = (Graphics2D) g;
+
+        g2d.translate(margin +5, margin +15);
+        g2d.setPaint(TemplateProperties.getFillColor());
+        g2d.fill(polygon);
+        g2d.setPaint(TemplateProperties.getLineColor());
+        g2d.draw(polygon);
+    }
+
+}
diff --git a/src/net/sf/openrocket/gui/print/TemplateProperties.java b/src/net/sf/openrocket/gui/print/TemplateProperties.java
new file mode 100644 (file)
index 0000000..f98f7ea
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * TemplateProperties.java
+ */
+package net.sf.openrocket.gui.print;
+
+import javax.swing.UIManager;
+import java.awt.Color;
+
+/**
+ * This class is responsible for managing various properties of print templates (fin, nose cone, transitions, etc.).
+ */
+public class TemplateProperties {
+
+    /**
+     * The property that defines the fill color.
+     */
+    public static final String TEMPLATE_FILL_COLOR_PROPERTY = "template.fill.color";
+
+    /**
+     * The property that defines the line color.
+     */
+    public static final String TEMPLATE_LINE_COLOR_PROPERTY = "template.line.color";
+
+    /**
+     * Get the current fill color.
+     * 
+     * @return  a color to be used as the fill in template shapes
+     */
+    public static Color getFillColor () {
+        Color fillColor = UIManager.getColor(TemplateProperties.TEMPLATE_FILL_COLOR_PROPERTY);
+        if (fillColor == null) {
+            fillColor = Color.lightGray;
+        }
+        return fillColor;
+    }
+
+    /**
+     * Get the current line color.
+     * 
+     * @return  a color to be used as the line in template shapes
+     */
+    public static Color getLineColor () {
+        Color lineColor = UIManager.getColor(TemplateProperties.TEMPLATE_LINE_COLOR_PROPERTY);
+        if (lineColor == null) {
+            lineColor = Color.darkGray;
+        }
+        return lineColor;
+    }
+}
diff --git a/src/net/sf/openrocket/gui/print/components/CheckBoxNode.java b/src/net/sf/openrocket/gui/print/components/CheckBoxNode.java
new file mode 100644 (file)
index 0000000..7fbab0a
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * CheckBoxNode.java
+ */
+package net.sf.openrocket.gui.print.components;
+
+/**
+ * A class that acts as the textual node of the check box within the JTree.
+ */
+public class CheckBoxNode {
+
+    /**
+     * The text label of the check box.
+     */
+    String text;
+
+    /**
+     * State flag indicating if the check box has been selected.
+     */
+    boolean selected;
+
+    /**
+     * Constructor.
+     * 
+     * @param theText  the check box label
+     * @param isSelected  true if selected
+     */
+    public CheckBoxNode (String theText, boolean isSelected) {
+        text = theText;
+        selected = isSelected;
+    }
+
+    /**
+     * Get the current state of the check box.
+     * 
+     * @return true if selected
+     */
+    public boolean isSelected () {
+        return selected;
+    }
+
+    /**
+     * Set the current state of the check box.  Note: this just tracks the state - it 
+     * does NOT actually set the state of the check box.
+     * 
+     * @param isSelected  true if selected
+     */
+    public void setSelected (boolean isSelected) {
+        selected = isSelected;
+    }
+
+    /**
+     * Get the text of the label.
+     * 
+     * @return  the text of the label
+     */
+    public String getText () {
+        return text;
+    }
+
+    /**
+     * Set the text of the label of the check box.
+     * 
+     * @param theText  the text of the label
+     */
+    public void setText (String theText) {
+        text = theText;
+    }
+
+    /**
+     * If someone prints this object, the text label will be displayed.
+     * 
+     * @return  the text label
+     */
+    public String toString () {
+        return text;
+    }
+}
\ No newline at end of file
diff --git a/src/net/sf/openrocket/gui/print/components/CheckTreeCellRenderer.java b/src/net/sf/openrocket/gui/print/components/CheckTreeCellRenderer.java
new file mode 100644 (file)
index 0000000..e4a9337
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * CheckTreeCellRenderer.java
+ */
+package net.sf.openrocket.gui.print.components;
+
+import javax.swing.JCheckBox;
+import javax.swing.JPanel;
+import javax.swing.JTree;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.DefaultTreeCellRenderer;
+import javax.swing.tree.TreeCellRenderer;
+import javax.swing.tree.TreePath;
+import java.awt.BorderLayout;
+import java.awt.Component;
+
+/**
+ * A cell renderer for JCheckBoxes within nodes of a JTree.
+ * <p/>
+ * Based in part on a blog by Santhosh Kumar.  http://www.jroller.com/santhosh/date/20050610
+ */
+public class CheckTreeCellRenderer extends JPanel implements TreeCellRenderer {
+
+    /**
+     * The selection model.
+     */
+    private CheckTreeSelectionModel selectionModel;
+    /**
+     * The delegated cell renderer.
+     */
+    private DefaultTreeCellRenderer delegate;
+    /**
+     * The check box within this cell.
+     */
+    private JCheckBox checkBox = new JCheckBox();
+
+    /**
+     * Constructor.
+     *
+     * @param theDelegate       the delegated cell renderer
+     * @param theSelectionModel the selection model
+     */
+    public CheckTreeCellRenderer (DefaultTreeCellRenderer theDelegate, CheckTreeSelectionModel theSelectionModel) {
+        delegate = theDelegate;
+
+        delegate.setLeafIcon(null);
+        delegate.setClosedIcon(null);
+        delegate.setOpenIcon(null);
+
+
+        selectionModel = theSelectionModel;
+        setLayout(new BorderLayout());
+        setOpaque(false);
+        checkBox.setOpaque(false);
+        checkBox.setSelected(true);
+    }
+
+    /**
+     * @{inheritDoc}
+     */
+    @Override
+    public Component getTreeCellRendererComponent (JTree tree, Object value, boolean selected, boolean expanded,
+                                                   boolean leaf, int row, boolean hasFocus) {
+        Component renderer = delegate.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row,
+                                                                   hasFocus);
+
+        TreePath path = tree.getPathForRow(row);
+        if (path != null) {
+            final boolean b = selectionModel.isPathSelected(path, true);
+            checkBox.setSelected(b);
+            if (value instanceof DefaultMutableTreeNode) {
+                Object obj = ((DefaultMutableTreeNode) value).getUserObject();
+                if (obj instanceof CheckBoxNode) {
+                    ((CheckBoxNode) obj).setSelected(b);
+                }
+            }
+        }
+
+        removeAll();
+        add(checkBox, BorderLayout.WEST);
+        add(renderer, BorderLayout.CENTER);
+        return this;
+    }
+} 
\ No newline at end of file
diff --git a/src/net/sf/openrocket/gui/print/components/CheckTreeManager.java b/src/net/sf/openrocket/gui/print/components/CheckTreeManager.java
new file mode 100644 (file)
index 0000000..719e50d
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * CheckTreeManager.java
+ */
+package net.sf.openrocket.gui.print.components;
+
+import javax.swing.JCheckBox;
+import javax.swing.JTree;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.tree.DefaultTreeCellRenderer;
+import javax.swing.tree.TreePath;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+
+/**
+ * This class manages mouse clicks within the JTree, handling selection/deselections within the JCheckBox of each cell in the tree.
+ */
+public class CheckTreeManager extends MouseAdapter implements TreeSelectionListener {
+    
+    /** The selection model. */
+    private CheckTreeSelectionModel selectionModel;
+    /** The actual JTree instance. */
+    private JTree tree;
+    /** The number of pixels of width of the check box.  Clicking anywhere within the box will trigger actions. */
+    int hotspot = new JCheckBox().getPreferredSize().width;
+
+    /**
+     * Construct a check box tree manager.
+     * 
+     * @param theTree  the actual tree being managed
+     */
+    public CheckTreeManager (RocketPrintTree theTree) {
+        tree = theTree;
+        selectionModel = new CheckTreeSelectionModel(tree.getModel());
+        theTree.setCheckBoxSelectionModel(selectionModel);
+        tree.setCellRenderer(new CheckTreeCellRenderer((DefaultTreeCellRenderer)tree.getCellRenderer(), selectionModel));
+        tree.addMouseListener(this);
+        selectionModel.addTreeSelectionListener(this);
+        
+        for (int x = 0; x < tree.getRowCount(); x++) {
+            tree.getSelectionModel().setSelectionPath(tree.getPathForRow(x));
+        }
+    }
+
+    public void addTreeSelectionListener (TreeSelectionListener tsl) {
+        selectionModel.addTreeSelectionListener(tsl);
+    }
+    
+    /**
+     * Called when the mouse clicks within the tree.
+     * 
+     * @param me  the event that triggered this
+     */
+    @Override
+    public void mouseClicked (MouseEvent me) {
+        TreePath path = tree.getPathForLocation(me.getX(), me.getY());
+        if (path == null) {
+            return;
+        }
+        if (me.getX() > tree.getPathBounds(path).x + hotspot) {
+            return;
+        }
+
+        boolean selected = selectionModel.isPathSelected(path, true);
+        selectionModel.removeTreeSelectionListener(this);
+
+        try {
+            if (selected) {
+                selectionModel.removeSelectionPath(path);
+            }
+            else {
+                selectionModel.addSelectionPath(path);
+            }
+        }
+        finally {
+            selectionModel.addTreeSelectionListener(this);
+            tree.treeDidChange();
+        }
+    }
+
+    /**
+     * Get the selection model being used by this manager.
+     * 
+     * @return the selection model
+     */
+    public CheckTreeSelectionModel getSelectionModel () {
+        return selectionModel;
+    }
+
+    /**
+     * Notify the tree that it changed.
+     * 
+     * @param e unused 
+     */
+    @Override
+    public void valueChanged (TreeSelectionEvent e) {
+        tree.treeDidChange();
+    }
+}
\ No newline at end of file
diff --git a/src/net/sf/openrocket/gui/print/components/CheckTreeSelectionModel.java b/src/net/sf/openrocket/gui/print/components/CheckTreeSelectionModel.java
new file mode 100644 (file)
index 0000000..9f34b4b
--- /dev/null
@@ -0,0 +1,245 @@
+/*
+ * CheckTreeSelectionModel.java
+ */
+package net.sf.openrocket.gui.print.components;
+
+import javax.swing.tree.DefaultTreeSelectionModel;
+import javax.swing.tree.TreeModel;
+import javax.swing.tree.TreePath;
+import javax.swing.tree.TreeSelectionModel;
+import java.util.ArrayList;
+import java.util.Stack;
+
+/**
+ * This class implements the selection model for the checkbox tree.  This specifically is used to keep
+ * track of the TreePaths that have a selected CheckBox.
+ */
+public class CheckTreeSelectionModel extends DefaultTreeSelectionModel {
+
+    /**
+     * The tree model.
+     */
+    private TreeModel model;
+
+    /**
+     * Constructor.
+     *
+     * @param theModel the model in use for the tree
+     */
+    public CheckTreeSelectionModel (TreeModel theModel) {
+        model = theModel;
+        setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
+    }
+
+    /**
+     * Tests whether there is any selected node in the subtree of given path.
+     *
+     * @param path the path to walk
+     *
+     * @return true if any item in the path or its descendants are selected
+     */
+    public boolean isPartiallySelected (TreePath path) {
+        if (isPathSelected(path, true)) {
+            return false;
+        }
+        TreePath[] selectionPaths = getSelectionPaths();
+        if (selectionPaths == null) {
+            return false;
+        }
+        for (TreePath selectionPath : selectionPaths) {
+            if (isDescendant(selectionPath, path)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Tells whether given path is selected. If dig is true, then a path is assumed to be selected, if one of its
+     * ancestor is selected.
+     *
+     * @param path the path to interrogate
+     * @param dig  if true then check if an ancestor is selected
+     *
+     * @return true if the path is selected
+     */
+    public boolean isPathSelected (TreePath path, boolean dig) {
+        if (!dig) {
+            return super.isPathSelected(path);
+        }
+        while (path != null && !super.isPathSelected(path)) {
+            path = path.getParentPath();
+        }
+        return path != null;
+    }
+
+    /**
+     * Determines if path1 is a descendant of path2.
+     *
+     * @param path1 descendant?
+     * @param path2 ancestor?
+     *
+     * @return true if path1 is a descendant of path2
+     */
+    private boolean isDescendant (TreePath path1, TreePath path2) {
+        Object obj1[] = path1.getPath();
+        Object obj2[] = path2.getPath();
+        for (int i = 0; i < obj2.length; i++) {
+            if (i < obj1.length) {
+                if (obj1[i] != obj2[i]) {
+                    return false;
+                }
+            }
+            else {
+                return false;
+            }
+        }
+        return true;
+    }
+
+
+    /**
+     * Unsupported exception.
+     *
+     * @param pPaths an array of paths
+     */
+    public void setSelectionPaths (TreePath[] pPaths) {
+        TreePath selected[] = getSelectionPaths();
+        for (TreePath aSelected : selected) {
+            removeSelectionPath(aSelected);
+        }
+        for (TreePath pPath : pPaths) {
+            addSelectionPath(pPath);
+        }
+    }
+
+    /**
+     * Add a set of TreePath nodes to the selection model.
+     *
+     * @param paths an array of tree path nodes
+     */
+    public void addSelectionPaths (TreePath[] paths) {
+        // deselect all descendants of paths[] 
+        for (TreePath path : paths) {
+            TreePath[] selectionPaths = getSelectionPaths();
+            if (selectionPaths == null) {
+                break;
+            }
+            ArrayList<TreePath> toBeRemoved = new ArrayList<TreePath>();
+            for (TreePath selectionPath : selectionPaths) {
+                if (isDescendant(selectionPath, path)) {
+                    toBeRemoved.add(selectionPath);
+                }
+            }
+            super.removeSelectionPaths(toBeRemoved.toArray(new TreePath[toBeRemoved.size()]));
+        }
+
+        // if all siblings are selected then deselect them and select parent recursively 
+        // otherwise just select that path. 
+        for (TreePath path : paths) {
+            TreePath temp = null;
+            while (areSiblingsSelected(path)) {
+                temp = path;
+                if (path.getParentPath() == null) {
+                    break;
+                }
+                path = path.getParentPath();
+            }
+            if (temp != null) {
+                if (temp.getParentPath() != null) {
+                    addSelectionPath(temp.getParentPath());
+                }
+                else {
+                    if (!isSelectionEmpty()) {
+                        removeSelectionPaths(getSelectionPaths());
+                    }
+                    super.addSelectionPaths(new TreePath[]{temp});
+                }
+            }
+            else {
+                super.addSelectionPaths(new TreePath[]{path});
+            }
+        }
+    }
+
+    /**
+     * Tells whether all siblings of given path are selected.
+     *
+     * @param path the tree path node
+     *
+     * @return true if all sibling nodes are selected
+     */
+    private boolean areSiblingsSelected (TreePath path) {
+        TreePath parent = path.getParentPath();
+        if (parent == null) {
+            return true;
+        }
+        Object node = path.getLastPathComponent();
+        Object parentNode = parent.getLastPathComponent();
+
+        int childCount = model.getChildCount(parentNode);
+        for (int i = 0; i < childCount; i++) {
+            Object childNode = model.getChild(parentNode, i);
+            if (childNode == node) {
+                continue;
+            }
+            if (!isPathSelected(parent.pathByAddingChild(childNode))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Remove paths from the selection model.
+     *
+     * @param paths the array of path nodes
+     */
+    public void removeSelectionPaths (TreePath[] paths) {
+        for (TreePath path : paths) {
+            if (path.getPathCount() == 1) {
+                super.removeSelectionPaths(new TreePath[]{path});
+            }
+            else {
+                toggleRemoveSelection(path);
+            }
+        }
+    }
+
+    /**
+     * If any ancestor node of given path is selected then deselect it and selection all its descendants except given
+     * path and descendants. otherwise just deselect the given path.
+     *
+     * @param path the tree path node
+     */
+    private void toggleRemoveSelection (TreePath path) {
+        Stack<TreePath> stack = new Stack<TreePath>();
+        TreePath parent = path.getParentPath();
+        while (parent != null && !isPathSelected(parent)) {
+            stack.push(parent);
+            parent = parent.getParentPath();
+        }
+        if (parent != null) {
+            stack.push(parent);
+        }
+        else {
+            super.removeSelectionPaths(new TreePath[]{path});
+            return;
+        }
+
+        while (!stack.isEmpty()) {
+            TreePath temp = stack.pop();
+            TreePath peekPath = stack.isEmpty() ? path : stack.peek();
+            Object node = temp.getLastPathComponent();
+            Object peekNode = peekPath.getLastPathComponent();
+            int childCount = model.getChildCount(node);
+            for (int i = 0; i < childCount; i++) {
+                Object childNode = model.getChild(node, i);
+                if (childNode != peekNode) {
+                    super.addSelectionPaths(new TreePath[]{temp.pathByAddingChild(childNode)});
+                }
+            }
+        }
+        super.removeSelectionPaths(new TreePath[]{parent});
+    }
+}
\ No newline at end of file
diff --git a/src/net/sf/openrocket/gui/print/components/RocketPrintTree.java b/src/net/sf/openrocket/gui/print/components/RocketPrintTree.java
new file mode 100644 (file)
index 0000000..b250abe
--- /dev/null
@@ -0,0 +1,252 @@
+/*
+ * RocketPrintTree.java
+ */
+package net.sf.openrocket.gui.print.components;
+
+import net.sf.openrocket.gui.print.OpenRocketPrintable;
+import net.sf.openrocket.gui.print.PrintableContext;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.Stage;
+
+import javax.swing.JTree;
+import javax.swing.event.TreeExpansionEvent;
+import javax.swing.event.TreeWillExpandListener;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.ExpandVetoException;
+import javax.swing.tree.TreePath;
+import javax.swing.tree.TreeSelectionModel;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Vector;
+
+/**
+ * A specialized JTree for displaying various rocket items that can be printed.
+ */
+public class RocketPrintTree extends JTree {
+
+    /**
+     * All check boxes are initially set to true (selected).
+     */
+    public static final boolean INITIAL_CHECKBOX_SELECTED = true;
+
+    /**
+     * The selection model that tracks the check box state.
+     */
+    private TreeSelectionModel theCheckBoxSelectionModel;
+
+    /**
+     * Constructor.
+     *
+     * @param root the vector of check box nodes (rows) to place into the tree
+     */
+    private RocketPrintTree (Vector root) {
+        super(root);
+
+        //Remove the little down and sideways arrows.  These are not needed because the tree expansion is fixed.
+        ((javax.swing.plaf.basic.BasicTreeUI) this.getUI()).
+                setExpandedIcon(null);
+        ((javax.swing.plaf.basic.BasicTreeUI) this.getUI()).
+                setCollapsedIcon(null);
+    }
+
+    /**
+     * Factory method to create a specialized JTree.  This version is for rocket's that have more than one stage.
+     *
+     * @param rocketName the name of the rocket
+     * @param stages     the array of all stages
+     *
+     * @return an instance of JTree
+     */
+    public static RocketPrintTree create (String rocketName, RocketComponent[] stages) {
+        Vector root = new Vector();
+        Vector toAddTo = root;
+
+        if (stages != null) {
+            if (stages.length > 1) {
+                final Vector parent = new NamedVector(rocketName != null ? rocketName : "Rocket");
+
+                root.add(parent);
+                toAddTo = parent;
+            }
+            for (RocketComponent stage : stages) {
+                if (stage instanceof Stage) {
+                    toAddTo.add(createNamedVector(stage.getName(), createPrintTreeNode(true), stage.getStageNumber()));
+                }
+            }
+        }
+        toAddTo.add(new CheckBoxNode(OpenRocketPrintable.DESIGN_REPORT.getDescription(),
+                                     INITIAL_CHECKBOX_SELECTED));
+
+        RocketPrintTree tree = new RocketPrintTree(root);
+
+        tree.addTreeWillExpandListener
+                (new TreeWillExpandListener() {
+                    public void treeWillExpand (TreeExpansionEvent e) {
+                    }
+
+                    public void treeWillCollapse (TreeExpansionEvent e)
+                            throws ExpandVetoException {
+                        throw new ExpandVetoException(e, "you can't collapse this JTree");
+                    }
+                });
+
+        return tree;
+    }
+
+    /**
+     * Factory method to create a specialized JTree.  This version is for a rocket with only one stage.
+     *
+     * @param rocketName the name of the rocket
+     *
+     * @return an instance of JTree
+     */
+    public static RocketPrintTree create (String rocketName) {
+        Vector root = new Vector();
+        root.add(new NamedVector(rocketName != null ? rocketName : "Rocket", createPrintTreeNode(false)));
+
+        RocketPrintTree tree = new RocketPrintTree(root);
+
+        tree.addTreeWillExpandListener
+                (new TreeWillExpandListener() {
+                    public void treeWillExpand (TreeExpansionEvent e) {
+                    }
+
+                    public void treeWillCollapse (TreeExpansionEvent e)
+                            throws ExpandVetoException {
+                        throw new ExpandVetoException(e, "you can't collapse this JTree");
+                    }
+                });
+
+        return tree;
+    }
+
+    /**
+     * This tree needs to have access both to the normal selection model (for the textual row) which is managed by the
+     * superclass, as well as the selection model for the check boxes.  This mutator method allows an external class to
+     * set the model back onto this class.  Because of some unfortunate circular dependencies this cannot be set at
+     * construction.
+     * <p/>
+     * TODO: Ensure these circular references get cleaned up properly at dialog disposal so everything can be GC'd.
+     *
+     * @param checkBoxSelectionModel the selection model used to keep track of the check box state
+     */
+    public void setCheckBoxSelectionModel (TreeSelectionModel checkBoxSelectionModel) {
+        theCheckBoxSelectionModel = checkBoxSelectionModel;
+    }
+
+    /**
+     * Add a selection path to the internal check box selection model.  The normal JTree selection model is unaffected.
+     * This has the effect of "selecting" the check box, but not highlighting the row.
+     *
+     * @param path the path (row)
+     */
+    public void addSelectionPath (TreePath path) {
+        theCheckBoxSelectionModel.addSelectionPath(path);
+    }
+
+    /**
+     * Helper to construct a named vector.
+     *
+     * @param name  the name of the vector
+     * @param nodes the array of nodes to put into the vector
+     * @param stage the stage number
+     *
+     * @return a NamedVector suitable for adding to a JTree
+     */
+    private static Vector createNamedVector (String name, CheckBoxNode[] nodes, int stage) {
+        return new NamedVector(name, nodes, stage);
+    }
+
+    /**
+     * Helper to construct the set of check box rows for each stage.
+     *
+     * @param onlyStageSpecific  if true then only stage specific OpenRocketPrintable rows are represented (in this part 
+     * of the tree).
+     * 
+     * @return an array of CheckBoxNode
+     */
+    private static CheckBoxNode[] createPrintTreeNode (boolean onlyStageSpecific) {
+        List<CheckBoxNode> nodes = new ArrayList<CheckBoxNode>();
+        OpenRocketPrintable[] printables = OpenRocketPrintable.values();
+        for (OpenRocketPrintable openRocketPrintable : printables) {
+            if (!onlyStageSpecific || openRocketPrintable.isStageSpecific() ) {
+                nodes.add(new CheckBoxNode(openRocketPrintable.getDescription(),
+                                           INITIAL_CHECKBOX_SELECTED));
+            }
+        }
+        return nodes.toArray(new CheckBoxNode[nodes.size()]);
+    }
+
+    /**
+     * Get the set of items to be printed, as selected by the user.
+     * 
+     * @return the things to be printed, returned as an Iterator<PrintableContext>
+     */
+    public Iterator<PrintableContext> getToBePrinted () {
+        final DefaultMutableTreeNode mutableTreeNode = (DefaultMutableTreeNode) getModel().getRoot();
+        PrintableContext pc = new PrintableContext();
+        add(pc, mutableTreeNode);
+        return pc.iterator();
+    }
+
+    /**
+     * Walk a tree, finding everything that has been selected and aggregating it into something that can be iterated upon
+     * This method is recursive.
+     * 
+     * @param pc                  the printable context that aggregates the choices into an iterator
+     * @param theMutableTreeNode  the root node
+     */
+    private void add (final PrintableContext pc, final DefaultMutableTreeNode theMutableTreeNode) {
+        int children = theMutableTreeNode.getChildCount();
+        for (int x = 0; x < children; x++) {
+
+            final DefaultMutableTreeNode at = (DefaultMutableTreeNode) theMutableTreeNode.getChildAt(x);
+            if (at.getUserObject() instanceof CheckBoxNode) {
+                CheckBoxNode cbn = (CheckBoxNode) at.getUserObject();
+                if (cbn.isSelected()) {
+                    final OpenRocketPrintable printable = OpenRocketPrintable.findByDescription(cbn.getText());
+                    pc.add(
+                            printable.isStageSpecific() ? ((NamedVector) theMutableTreeNode.getUserObject())
+                                    .getStage() : null,
+                            printable);
+                }
+            }
+            add(pc, at);
+        }
+    }
+
+}
+
+/**
+ * JTree's work off of Vector's (unfortunately).  This class is tailored for use with check boxes in the JTree.
+ */
+class NamedVector extends Vector<CheckBoxNode> {
+    String name;
+
+    int stageNumber;
+
+    public NamedVector (String theName) {
+        name = theName;
+    }
+
+    public NamedVector (String theName, CheckBoxNode elements[], int stage) {
+        this(theName, elements);
+        stageNumber = stage;
+    }
+
+    public NamedVector (String theName, CheckBoxNode elements[]) {
+        name = theName;
+        for (int i = 0, n = elements.length; i < n; i++) {
+            add(elements[i]);
+        }
+    }
+
+    public String toString () {
+        return name;
+    }
+
+    public int getStage () {
+        return stageNumber;
+    }
+}
\ No newline at end of file
diff --git a/src/net/sf/openrocket/gui/print/visitor/BaseVisitorStrategy.java b/src/net/sf/openrocket/gui/print/visitor/BaseVisitorStrategy.java
new file mode 100644 (file)
index 0000000..1f7852b
--- /dev/null
@@ -0,0 +1,333 @@
+/*
+ * BaseVisitorStrategy.java
+ */
+package net.sf.openrocket.gui.print.visitor;
+
+import com.itextpdf.text.Document;
+import com.itextpdf.text.pdf.PdfWriter;
+import net.sf.openrocket.rocketcomponent.BodyComponent;
+import net.sf.openrocket.rocketcomponent.BodyTube;
+import net.sf.openrocket.rocketcomponent.ComponentVisitor;
+import net.sf.openrocket.rocketcomponent.ComponentVisitorStrategy;
+import net.sf.openrocket.rocketcomponent.EllipticalFinSet;
+import net.sf.openrocket.rocketcomponent.ExternalComponent;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.InnerTube;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.RadiusRingComponent;
+import net.sf.openrocket.rocketcomponent.RingComponent;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.Stage;
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * This abstract class contains boilerplate functionality to support visiting the components of a rocket.
+ * It is a visitor strategy, not a visitor per se.
+ */
+public abstract class BaseVisitorStrategy implements ComponentVisitorStrategy {
+
+    /**
+     * The owning visitor.
+     */
+    protected ComponentVisitor parent;
+
+    /**
+     * The iText document.
+     */
+    protected Document document;
+
+    /**
+     * The direct iText writer.
+     */
+    protected PdfWriter writer;
+
+    /**
+     * The stages selected.
+     */
+    protected Set<Integer> stages;
+    
+    /**
+     * State variable to track the level of hierarchy.
+     */
+    protected int level = 0;
+
+    /**
+     * Default no-arg constructor.
+     */
+    public BaseVisitorStrategy () {
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param doc         the iText document
+     */
+    public BaseVisitorStrategy (Document doc) {
+        this(doc, null);
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param doc         the iText document
+     * @param theWriter   an iText byte writer
+     */
+    public BaseVisitorStrategy (Document doc, PdfWriter theWriter) {
+        this(doc, theWriter, new HashSet<Integer>());
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param doc         the iText document
+     * @param theWriter   an iText byte writer
+     * @param theStages   a set of stage numbers
+     */
+    public BaseVisitorStrategy (Document doc, PdfWriter theWriter, Set<Integer> theStages) {
+        document = doc;
+        writer = theWriter;
+        stages = theStages;
+    }
+
+    /**
+     * Determine if the visitor strategy's set of stage numbers (to print) contains the specified stage.
+     * 
+     * @param stageNumber a stage number
+     * 
+     * @return  true if the visitor strategy contains the stage number provided
+     */
+    public boolean shouldVisitStage (int stageNumber) {
+        if (stages == null || stages.isEmpty()) {
+            return false;
+        }
+
+        for (final Integer stage : stages) {
+            if (stage == stageNumber) {
+                return true;
+            }
+        }
+        
+        return false;
+    }
+    
+    /**
+     * Recurse through the given rocket component.
+     * 
+     * @param root  the root component; all children will be visited recursively
+     */
+    protected void goDeep (final RocketComponent root) {
+        RocketComponent[] rc = root.getChildren();
+        goDeep(rc);
+    }
+
+
+    /**
+      * Recurse through the given rocket component.
+      * 
+      * @param theRc  an array of rocket components; all children will be visited recursively
+      */
+     protected void goDeep (final RocketComponent[] theRc) {
+        level++;
+        for (RocketComponent rocketComponent : theRc) {
+            rocketComponent.accept(parent);
+        }
+        level--;
+    }
+
+    /**
+     * Get the dimensions of the paper page.
+     * 
+     * @return an internal Dimension
+     */
+    protected Dimension getPageSize() {
+        return new Dimension(document.getPageSize().getWidth(),
+                             document.getPageSize().getHeight());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final Rocket visitable) {
+        goDeep(visitable);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final RocketComponent visitable) {
+        if (shouldVisitStage(visitable.getStageNumber())) {
+            goDeep(visitable);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final Stage visitable) {
+        if (shouldVisitStage(visitable.getStageNumber())) {
+            goDeep(visitable);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final ExternalComponent visitable) {
+        if (shouldVisitStage(visitable.getStageNumber())) {
+            goDeep(visitable);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final BodyComponent visitable) {
+        if (shouldVisitStage(visitable.getStageNumber())) {
+            goDeep(visitable);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final RingComponent visitable) {
+        if (shouldVisitStage(visitable.getStageNumber())) {
+            goDeep(visitable);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final InnerTube visitable) {
+        if (shouldVisitStage(visitable.getStageNumber())) {
+            goDeep(visitable);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final LaunchLug visitable) {
+        if (shouldVisitStage(visitable.getStageNumber())) {
+            goDeep(visitable);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final Transition visitable) {
+        if (shouldVisitStage(visitable.getStageNumber())) {
+            goDeep(visitable);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final RadiusRingComponent visitable) {
+        if (shouldVisitStage(visitable.getStageNumber())) {
+            goDeep(visitable);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final NoseCone visitable) {
+        if (shouldVisitStage(visitable.getStageNumber())) {
+            goDeep(visitable);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final BodyTube visitable) {
+        if (shouldVisitStage(visitable.getStageNumber())) {
+            goDeep(visitable);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final TrapezoidFinSet visitable) {
+        if (shouldVisitStage(visitable.getStageNumber())) {
+            goDeep(visitable);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final EllipticalFinSet visitable) {
+        if (shouldVisitStage(visitable.getStageNumber())) {
+            goDeep(visitable);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final FreeformFinSet visitable) {
+        if (shouldVisitStage(visitable.getStageNumber())) {
+            goDeep(visitable);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setParent (final ComponentVisitor theParent) {
+        parent = theParent;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void close () {
+    }
+}
+
+class Dimension {
+    public float width;
+    public float height;
+    
+    public Dimension(float w, float h) {
+        width = w;
+        height = h;
+    }
+    
+    public float getWidth() {
+        return width;
+    }
+    
+    public float getHeight() {
+        return height;
+    }
+}
\ No newline at end of file
diff --git a/src/net/sf/openrocket/gui/print/visitor/FinSetVisitorStrategy.java b/src/net/sf/openrocket/gui/print/visitor/FinSetVisitorStrategy.java
new file mode 100644 (file)
index 0000000..8711039
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * FinSetVisitorStrategy.java
+ */
+package net.sf.openrocket.gui.print.visitor;
+
+import com.itextpdf.text.Document;
+import com.itextpdf.text.pdf.PdfContentByte;
+import com.itextpdf.text.pdf.PdfWriter;
+import net.sf.openrocket.gui.print.PrintableFinSet;
+import net.sf.openrocket.rocketcomponent.EllipticalFinSet;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
+
+import java.awt.Graphics2D;
+import java.util.Set;
+
+/**
+ * A visitor strategy for drawing fin templates.
+ */
+public class FinSetVisitorStrategy extends BaseVisitorStrategy {
+
+    /**
+     * Constructor.
+     * 
+     * @param doc              The iText document
+     * @param theWriter        The direct iText writer
+     * @param theStagesToVisit The stages to be visited by this strategy
+     */
+    public FinSetVisitorStrategy (Document doc, PdfWriter theWriter, Set<Integer> theStagesToVisit) {
+        super(doc, theWriter, theStagesToVisit);
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final TrapezoidFinSet visitable) {
+        doVisit(visitable);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final EllipticalFinSet visitable) {
+        doVisit(visitable);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final FreeformFinSet visitable) {
+        doVisit(visitable);
+    }
+
+    /**
+     * The core behavior of this visitor.
+     * 
+     * @param visitable  the object to extract info about; a graphical image of the fin shape is drawn to the document
+     */
+    private void doVisit (final FinSet visitable) {
+        if (shouldVisitStage(visitable.getStageNumber())) {
+            PrintableFinSet pfs = new PrintableFinSet(visitable);
+
+            net.sf.openrocket.gui.print.visitor.Dimension d = getPageSize();
+            PdfContentByte cb = writer.getDirectContent();
+            Graphics2D g2 = cb.createGraphics(d.width, d.height);
+            pfs.print(g2);
+            g2.dispose();
+            document.newPage();
+        }
+    }
+}
+
diff --git a/src/net/sf/openrocket/gui/print/visitor/MotorMountVisitorStrategy.java b/src/net/sf/openrocket/gui/print/visitor/MotorMountVisitorStrategy.java
new file mode 100644 (file)
index 0000000..579cb2d
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * MotorMountVisitor.java
+ */
+package net.sf.openrocket.gui.print.visitor;
+
+import com.itextpdf.text.Document;
+import net.sf.openrocket.motor.Motor;
+import net.sf.openrocket.rocketcomponent.BodyTube;
+import net.sf.openrocket.rocketcomponent.InnerTube;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A visitor strategy for finding data about motor configurations.  This visitor accumulates information about the
+ * motors currently 'installed' into each motor mount in the rocket.  When the visitor is complete, invoke {@link
+ * #getMotors()} to obtain the list of Motor instances that correspond to the motor configuration.
+ */
+public class MotorMountVisitorStrategy extends BaseVisitorStrategy {
+
+    /**
+     * The motor configuration identifier.
+     */
+    private String mid;
+
+    /** The accumulating list of motors. */
+    private List<Motor> motors = new ArrayList<Motor>();
+
+    /**
+     * Constructor.
+     *
+     * @param doc           the iText document
+     * @param motorConfigID the motor configuration ID
+     */
+    public MotorMountVisitorStrategy (Document doc,
+                                      String motorConfigID) {
+        super(doc);
+        mid = motorConfigID;
+    }
+
+    /**
+     * Override the method that determines if the visiting should be going deep.
+     *
+     * @param stageNumber a stage number
+     *
+     * @return true, always
+     */
+    public boolean shouldVisitStage (int stageNumber) {
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final BodyTube visitable) {
+        if (visitable.isMotorMount()) {
+            doVisit(visitable);
+        }
+        else {
+            goDeep(visitable);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final InnerTube visitable) {
+        if (visitable.isMotorMount()) {
+            doVisit(visitable);
+        }
+    }
+
+    /**
+     * The core behavior of this visitor.
+     *
+     * @param visitable the object to extract info about; a graphical image of the fin shape is drawn to the document
+     */
+    private void doVisit (final MotorMount visitable) {
+        final Motor motor = visitable.getMotor(mid);
+        if (motor != null) {
+            motors.add(motor);
+        }
+    }
+
+    /**
+     * Answer with the list of motors that have been accumulated from visiting all of the motor mount components in the
+     * rocket component hierarchy.
+     *
+     * @return a list of motors
+     */
+    public List<Motor> getMotors () {
+        return motors;
+    }
+
+}
+
+
diff --git a/src/net/sf/openrocket/gui/print/visitor/PartsDetailVisitorStrategy.java b/src/net/sf/openrocket/gui/print/visitor/PartsDetailVisitorStrategy.java
new file mode 100644 (file)
index 0000000..274f8d3
--- /dev/null
@@ -0,0 +1,434 @@
+/*
+ * PartsDetailVisitorStrategy.java
+ */
+package net.sf.openrocket.gui.print.visitor;
+
+import com.itextpdf.text.BadElementException;
+import com.itextpdf.text.Chunk;
+import com.itextpdf.text.Document;
+import com.itextpdf.text.DocumentException;
+import com.itextpdf.text.Element;
+import com.itextpdf.text.Font;
+import com.itextpdf.text.Image;
+import com.itextpdf.text.Phrase;
+import com.itextpdf.text.Rectangle;
+import com.itextpdf.text.pdf.PdfPCell;
+import com.itextpdf.text.pdf.PdfPTable;
+import com.itextpdf.text.pdf.PdfWriter;
+import net.sf.openrocket.gui.main.ComponentIcons;
+import net.sf.openrocket.gui.print.ITextHelper;
+import net.sf.openrocket.gui.print.PrintUtilities;
+import net.sf.openrocket.gui.print.PrintableFinSet;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.BodyComponent;
+import net.sf.openrocket.rocketcomponent.BodyTube;
+import net.sf.openrocket.rocketcomponent.Coaxial;
+import net.sf.openrocket.rocketcomponent.EllipticalFinSet;
+import net.sf.openrocket.rocketcomponent.ExternalComponent;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.InnerTube;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.RadiusRingComponent;
+import net.sf.openrocket.rocketcomponent.RingComponent;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.Stage;
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.Coordinate;
+
+import javax.swing.ImageIcon;
+import java.io.IOException;
+import java.text.NumberFormat;
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * A visitor strategy for creating documentation about parts details.
+ */
+public class PartsDetailVisitorStrategy extends BaseVisitorStrategy {
+
+    /**
+     * The number of columns in the table.
+     */
+    private static final int TABLE_COLUMNS = 7;
+
+    /**
+     * The parts detail is represented as an iText table.
+     */
+    PdfPTable grid;
+
+    /**
+     * Construct a strategy for visiting a parts hierarchy for the purposes of collecting details on those parts.
+     *
+     * @param doc              The iText document
+     * @param theWriter        The direct iText writer
+     * @param theStagesToVisit The stages to be visited by this strategy
+     */
+    public PartsDetailVisitorStrategy (Document doc, PdfWriter theWriter, Set<Integer> theStagesToVisit) {
+        super(doc, theWriter, theStagesToVisit);
+        PrintUtilities.addText(doc, PrintUtilities.BIG_BOLD, "Parts Detail");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final Stage visitable) {
+        try {
+            if (grid != null) {
+                document.add(grid);
+            }
+            document.add(ITextHelper.createPhrase(visitable.getName()));
+            grid = new PdfPTable(TABLE_COLUMNS);
+            grid.setWidthPercentage(100);
+            grid.setHorizontalAlignment(Element.ALIGN_LEFT);
+        }
+        catch (DocumentException e) {
+        }
+
+        RocketComponent[] rc = visitable.getChildren();
+        goDeep(rc);
+    }
+
+    
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final ExternalComponent visitable) {
+        grid.addCell(iconToImage(visitable));
+        grid.addCell(createNameCell(visitable.getName(), true));
+
+        grid.addCell(createMaterialCell(visitable.getMaterial()));
+        grid.addCell(ITextHelper.createCell());
+        grid.addCell(createLengthCell(visitable.getLength()));
+        grid.addCell(createMassCell(visitable.getMass()));
+
+        RocketComponent[] rc = visitable.getChildren();
+        goDeep(rc);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final BodyComponent visitable) {
+        grid.addCell(visitable.getName());
+        grid.completeRow();
+        RocketComponent[] rc = visitable.getChildren();
+        goDeep(rc);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final RingComponent visitable) {
+        grid.addCell(iconToImage(visitable));
+        grid.addCell(createNameCell(visitable.getName(), true));
+        grid.addCell(createMaterialCell(visitable.getMaterial()));
+        grid.addCell(createOuterDiaCell(visitable));
+        grid.addCell(createLengthCell(visitable.getLength()));
+        grid.addCell(createMassCell(visitable.getMass()));
+
+        RocketComponent[] rc = visitable.getChildren();
+        goDeep(rc);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final InnerTube visitable) {
+        grid.addCell(iconToImage(visitable));
+        final PdfPCell pCell = createNameCell(visitable.getName(), true);
+        grid.addCell(pCell);
+        grid.addCell(createMaterialCell(visitable.getMaterial()));
+        grid.addCell(createOuterDiaCell(visitable));
+        grid.addCell(createLengthCell(visitable.getLength()));
+        grid.addCell(createMassCell(visitable.getMass()));
+
+        RocketComponent[] rc = visitable.getChildren();
+        goDeep(rc);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final LaunchLug visitable) {
+        grid.addCell(iconToImage(visitable));
+        grid.addCell(createNameCell(visitable.getName(), true));
+
+        grid.addCell(createMaterialCell(visitable.getMaterial()));
+        grid.addCell(createOuterDiaCell(visitable));
+        grid.addCell(createLengthCell(visitable.getLength()));
+        grid.addCell(createMassCell(visitable.getMass()));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final Transition visitable) {
+        grid.addCell(iconToImage(visitable));
+        grid.addCell(createNameCell(visitable.getName(), true));
+        grid.addCell(createMaterialCell(visitable.getMaterial()));
+
+        Chunk fore = new Chunk("Fore Dia: " + appendLength(visitable.getForeRadius() * 2));
+        fore.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE));
+        Chunk aft = new Chunk("Aft Dia: " + appendLength(visitable.getAftRadius() * 2));
+        aft.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE));
+        final PdfPCell cell = ITextHelper.createCell();
+        cell.addElement(fore);
+        cell.addElement(aft);
+        grid.addCell(cell);
+        grid.addCell(createLengthCell(visitable.getLength()));
+        grid.addCell(createMassCell(visitable.getMass()));
+
+        RocketComponent[] rc = visitable.getChildren();
+        goDeep(rc);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final RadiusRingComponent visitable) {
+        grid.addCell(iconToImage(visitable));
+        grid.addCell(createNameCell(visitable.getName(), true));
+        grid.addCell(createMaterialCell(visitable.getMaterial()));
+        grid.addCell(createOuterDiaCell(visitable));
+        grid.addCell(createLengthCell(visitable.getLength()));
+        grid.addCell(createMassCell(visitable.getMass()));
+        RocketComponent[] rc = visitable.getChildren();
+        goDeep(rc);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final NoseCone visitable) {
+        grid.addCell(iconToImage(visitable));
+        grid.addCell(createNameCell(visitable.getName(), true));
+        grid.addCell(createMaterialCell(visitable.getMaterial()));
+        grid.addCell(ITextHelper.createCell(visitable.getType().getName(), PdfPCell.BOTTOM));
+        grid.addCell(createLengthCell(visitable.getLength()));
+        grid.addCell(createMassCell(visitable.getMass()));
+        RocketComponent[] rc = visitable.getChildren();
+        goDeep(rc);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final BodyTube visitable) {
+        grid.addCell(iconToImage(visitable));
+        grid.addCell(createNameCell(visitable.getName(), true));
+        grid.addCell(createMaterialCell(visitable.getMaterial()));
+        grid.addCell(createOuterDiaCell(visitable));
+        grid.addCell(createLengthCell(visitable.getLength()));
+        grid.addCell(createMassCell(visitable.getMass()));
+        RocketComponent[] rc = visitable.getChildren();
+        goDeep(rc);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final TrapezoidFinSet visitable) {
+        visitFins(visitable);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final EllipticalFinSet visitable) {
+        visitFins(visitable);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final FreeformFinSet visitable) {
+        visitFins(visitable);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void close () {
+        try {
+            if (grid != null) {
+                document.add(grid);
+            }
+        }
+        catch (DocumentException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private PdfPCell createOuterDiaCell (final Coaxial visitable) {
+        PdfPCell result = new PdfPCell();
+        Phrase p = new Phrase();
+        p.setLeading(12f);
+        result.setVerticalAlignment(Element.ALIGN_TOP);
+        result.setBorder(Rectangle.BOTTOM);
+        Chunk c = new Chunk();
+        c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE));
+        c.append("Dia");
+        p.add(c);
+
+        c = new Chunk();
+        c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.SMALL_FONT_SIZE));
+        c.append("out");
+        p.add(c);
+
+        c = new Chunk();
+        c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE));
+        c.append(" " + appendLength(visitable.getOuterRadius() * 2));
+        p.add(c);
+        createInnerDiaCell(visitable, result);
+        result.addElement(p);
+        return result;
+    }
+
+    private void createInnerDiaCell (final Coaxial visitable, PdfPCell cell) {
+        Phrase p = new Phrase();
+        p.setLeading(14f);
+        Chunk c = new Chunk();
+        c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE));
+        c.append("Dia");
+        p.add(c);
+
+        c = new Chunk();
+        c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.SMALL_FONT_SIZE));
+        c.append("in ");
+        p.add(c);
+
+        c = new Chunk();
+        c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE));
+        c.append("  " + appendLength(visitable.getInnerRadius() * 2));
+        p.add(c);
+        cell.addElement(p);
+    }
+
+    private void visitFins (FinSet visitable) {
+
+        Image img = null;
+        java.awt.Image awtImage = new PrintableFinSet(visitable).createImage();
+
+        Collection<Coordinate> x = visitable.getComponentBounds();
+
+        try {
+            img = Image.getInstance(writer, awtImage, 0.25f);
+        }
+        catch (BadElementException e) {
+            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
+        }
+        catch (IOException e) {
+            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
+        }
+
+        grid.addCell(iconToImage(visitable));
+        grid.addCell(createNameCell(visitable.getName() + " (" + visitable.getFinCount() + ")", true));
+        grid.addCell(createMaterialCell(visitable.getMaterial()));
+        grid.addCell(ITextHelper.createCell("Thick: " + appendLength(visitable.getThickness()), PdfPCell.BOTTOM));
+        final PdfPCell pCell = new PdfPCell();
+        pCell.setBorder(Rectangle.BOTTOM);
+        pCell.addElement(img);
+
+        grid.addCell(ITextHelper.createCell());
+        grid.addCell(createMassCell(visitable.getMass()));
+
+        RocketComponent[] rc = visitable.getChildren();
+        goDeep(rc);
+    }
+
+    protected PdfPCell createLengthCell (double length) {
+        return ITextHelper.createCell("Len: " + appendLength(length), PdfPCell.BOTTOM);
+    }
+
+    protected PdfPCell createMassCell (double mass) {
+        return ITextHelper.createCell("Mass: " + appendMass(mass), PdfPCell.BOTTOM);
+    }
+
+    protected PdfPCell createNameCell (String v, boolean withIndent) {
+        PdfPCell result = new PdfPCell();
+        result.setBorder(Rectangle.BOTTOM);
+        Chunk c = new Chunk();
+        c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE));
+        if (withIndent) {
+            for (int x = 0; x < (level - 2) * 10; x++) {
+                c.append(" ");
+            }
+        }
+        c.append(v);
+        result.setColspan(2);
+        result.addElement(c);
+        return result;
+    }
+
+    protected PdfPCell createMaterialCell (Material material) {
+        PdfPCell cell = ITextHelper.createCell();
+        cell.setLeading(13f, 0);
+
+        Chunk c = new Chunk();
+        c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE));
+        c.append(appendMaterial(material));
+        cell.addElement(c);
+        Chunk density = new Chunk();
+        density.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.SMALL_FONT_SIZE));
+        density.append(appendMaterialDensity(material));
+        cell.addElement(density);
+        return cell;
+    }
+
+    protected PdfPCell iconToImage (final RocketComponent visitable) {
+        final ImageIcon icon = (ImageIcon) ComponentIcons.getLargeIcon(visitable.getClass());
+        try {
+            Image im = Image.getInstance(icon.getImage(), null);
+            im.scaleToFit(icon.getIconWidth() * 0.6f, icon.getIconHeight() * 0.6f);
+            PdfPCell cell = new PdfPCell(im);
+            cell.setFixedHeight(icon.getIconHeight() * 0.6f);
+            cell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
+            cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
+            cell.setBorder(PdfPCell.NO_BORDER);
+            return cell;
+        }
+        catch (BadElementException e) {
+        }
+        catch (IOException e) {
+        }
+        return null;
+    }
+
+    protected String appendLength (double length) {
+        final Unit defaultUnit = UnitGroup.UNITS_LENGTH.getDefaultUnit();
+        return NumberFormat.getNumberInstance().format(defaultUnit.toUnit(length)) + defaultUnit.toString();
+    }
+
+    protected String appendMass (double mass) {
+        final Unit defaultUnit = UnitGroup.UNITS_MASS.getDefaultUnit();
+        return NumberFormat.getNumberInstance().format(defaultUnit.toUnit(mass)) + defaultUnit.toString();
+    }
+
+    protected String appendMaterial (Material material) {
+        return material.getName();
+    }
+
+    protected String appendMaterialDensity (Material material) {
+        return " (" + material.getType().getUnitGroup().getDefaultUnit().toStringUnit(material.getDensity()) + ")";
+    }
+
+}
diff --git a/src/net/sf/openrocket/gui/print/visitor/PartsListVisitorStrategy.java b/src/net/sf/openrocket/gui/print/visitor/PartsListVisitorStrategy.java
new file mode 100644 (file)
index 0000000..f6ebab8
--- /dev/null
@@ -0,0 +1,256 @@
+/*
+ * PartsListVisitorStrategy.java
+ */
+package net.sf.openrocket.gui.print.visitor;
+
+import com.itextpdf.text.Document;
+import com.itextpdf.text.pdf.PdfWriter;
+import net.sf.openrocket.rocketcomponent.BodyTube;
+import net.sf.openrocket.rocketcomponent.Coaxial;
+import net.sf.openrocket.rocketcomponent.ComponentVisitor;
+import net.sf.openrocket.rocketcomponent.EllipticalFinSet;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.InnerTube;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.RadiusRingComponent;
+import net.sf.openrocket.rocketcomponent.RingComponent;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A visitor strategy for creating documentation about a parts list.
+ */
+public class PartsListVisitorStrategy extends BaseVisitorStrategy {
+
+    /**
+     * Accumulator for parts data.
+     */
+    private Map<PartsAccumulator, PartsAccumulator> crap = new HashMap<PartsAccumulator, PartsAccumulator>();
+    
+    /**
+     * Construct a strategy for visiting a parts hierarchy for the purposes of collecting details on those parts.
+     *
+     * @param doc              The iText document
+     * @param theWriter        The direct iText writer
+     * @param theStagesToVisit The stages to be visited by this strategy
+     */    
+    public PartsListVisitorStrategy (Document doc, PdfWriter theWriter, Set<Integer> theStagesToVisit) {
+        super(doc, theWriter, theStagesToVisit);
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final RingComponent visitable) {
+        final PartsAccumulator key = new PartsAccumulator(visitable);
+            PartsAccumulator pa = crap.get(key);
+        if (pa == null) {
+            pa = key;
+            crap.put(pa, pa);
+        }
+        pa.increment();
+        RocketComponent[] rc = visitable.getChildren();
+        goDeep(rc);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final InnerTube visitable) {
+        final PartsAccumulator key = new PartsAccumulator(visitable);
+            PartsAccumulator pa = crap.get(key);
+        if (pa == null) {
+            pa = key;
+            crap.put(pa, pa);
+        }
+        pa.increment();
+        RocketComponent[] rc = visitable.getChildren();
+        goDeep(rc);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final LaunchLug visitable) {
+        final PartsAccumulator key = new PartsAccumulator(visitable);
+            PartsAccumulator pa = crap.get(key);
+        if (pa == null) {
+            pa = key;
+            crap.put(pa, pa);
+        }
+        pa.increment();
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final Transition visitable) {
+        final PartsAccumulator key = new PartsAccumulator(visitable);
+            PartsAccumulator pa = crap.get(key);
+        if (pa == null) {
+            pa = key;
+            crap.put(pa, pa);
+        }
+        pa.increment();
+        RocketComponent[] rc = visitable.getChildren();
+        goDeep(rc);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final RadiusRingComponent visitable) {
+        final PartsAccumulator key = new PartsAccumulator(visitable);
+            PartsAccumulator pa = crap.get(key);
+        if (pa == null) {
+            pa = key;
+            crap.put(pa, pa);
+        }
+        pa.increment();
+        RocketComponent[] rc = visitable.getChildren();
+        goDeep(rc);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final NoseCone visitable) {
+        final PartsAccumulator key = new PartsAccumulator(visitable);
+            PartsAccumulator pa = crap.get(key);
+        if (pa == null) {
+            pa = key;
+            crap.put(pa, pa);
+        }
+        pa.increment();
+        RocketComponent[] rc = visitable.getChildren();
+        goDeep(rc);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final BodyTube visitable) {
+        final PartsAccumulator key = new PartsAccumulator(visitable);
+            PartsAccumulator pa = crap.get(key);
+        if (pa == null) {
+            pa = key;
+            crap.put(pa, pa);
+        }
+        pa.increment();
+        RocketComponent[] rc = visitable.getChildren();
+        goDeep(rc);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final TrapezoidFinSet visitable) {
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final EllipticalFinSet visitable) {
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void visit (final FreeformFinSet visitable) {
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void setParent (final ComponentVisitor theParent) {
+        parent = theParent;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override 
+    public void close () {
+        for (PartsAccumulator partsAccumulator : crap.keySet()) {
+            System.err.println(partsAccumulator.component.getComponentName() + " " + partsAccumulator.quantity);
+        }
+    }
+    
+}
+
+class PartsAccumulator {
+    
+    int quantity = 0;
+    
+    RocketComponent component;
+    
+    PartsAccumulator (RocketComponent theComponent) {
+        component = theComponent;
+    }
+
+    void increment() {
+        quantity++;
+    }
+    
+    int quantity() {
+        return quantity;
+    }
+    
+    public boolean equals (final Object o1) {
+        if (this == o1) {
+            return true;
+        }
+        
+        RocketComponent that;
+        if (o1 instanceof net.sf.openrocket.gui.print.visitor.PartsAccumulator) {
+            that = ((net.sf.openrocket.gui.print.visitor.PartsAccumulator)o1).component;
+        }
+        else if (o1 instanceof RocketComponent) {
+            that = (RocketComponent)o1;
+        }
+        else {
+            return false;
+        }
+        
+        if (this.component.getClass().equals(that.getClass())) {
+            //If 
+            if (that.getLength() == this.component.getLength()) {
+                if (that.getMass() == this.component.getMass()) {
+                    return true;
+                }
+            }
+            if (this.component instanceof Coaxial &&
+                that instanceof Coaxial) {
+                Coaxial cThis = (Coaxial)this.component;
+                Coaxial cThat = (Coaxial)that;
+                if (cThis.getInnerRadius() == cThat.getInnerRadius() &&
+                        cThis.getOuterRadius() == cThat.getOuterRadius()) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        return false;
+    }
+    
+    public int hashCode() {
+        return component.getComponentName().hashCode();
+    }
+}
\ No newline at end of file
diff --git a/src/net/sf/openrocket/gui/print/visitor/StageVisitorStrategy.java b/src/net/sf/openrocket/gui/print/visitor/StageVisitorStrategy.java
new file mode 100644 (file)
index 0000000..19e81e7
--- /dev/null
@@ -0,0 +1,249 @@
+/*
+ * StageVisitor.java
+ */
+package net.sf.openrocket.gui.print.visitor;
+
+import net.sf.openrocket.rocketcomponent.BodyComponent;
+import net.sf.openrocket.rocketcomponent.BodyTube;
+import net.sf.openrocket.rocketcomponent.ComponentVisitor;
+import net.sf.openrocket.rocketcomponent.ComponentVisitorStrategy;
+import net.sf.openrocket.rocketcomponent.EllipticalFinSet;
+import net.sf.openrocket.rocketcomponent.ExternalComponent;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.InnerTube;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.RadiusRingComponent;
+import net.sf.openrocket.rocketcomponent.RingComponent;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.Stage;
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This visitor strategy accumulates Stage references in a Rocket hierarchy.
+ */
+public class StageVisitorStrategy implements ComponentVisitorStrategy {
+
+    /**
+     * The collection of stages, accumulated during a visitation.
+     */
+    private List<Double> stageComponents = new ArrayList<Double>();
+
+    private Double mass = 0d;
+
+    /**
+     * The owning visitor.
+     */
+    protected ComponentVisitor parent;
+
+    /**
+     * Constructor.
+     */
+    public StageVisitorStrategy () {
+    }
+
+    /**
+     * Override the method that determines if the visiting should be going deep.
+     *
+     * @param stageNumber a stage number
+     *
+     * @return true, always
+     */
+    public boolean shouldVisitStage (int stageNumber) {
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setParent (final ComponentVisitor theParent) {
+        parent = theParent;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final Rocket visitable) {
+        goDeep(visitable);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final Stage visitable) {
+
+        if (mass > 0d) {
+            stageComponents.add(mass);
+        }
+        mass = 0d;
+        RocketComponent[] rc = visitable.getChildren();
+        goDeep(rc);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final RocketComponent visitable) {
+        mass += visitable.getMass();
+        goDeep(visitable);
+    }
+
+    /**
+     * Recurse through the given rocket component.
+     *
+     * @param root the root component; all children will be visited recursively
+     */
+    protected void goDeep (final RocketComponent root) {
+        RocketComponent[] rc = root.getChildren();
+        goDeep(rc);
+    }
+
+
+    /**
+     * Recurse through the given rocket component.
+     *
+     * @param theRc an array of rocket components; all children will be visited recursively
+     */
+    protected void goDeep (final RocketComponent[] theRc) {
+        for (RocketComponent rocketComponent : theRc) {
+            rocketComponent.accept(parent);
+        }
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final ExternalComponent visitable) {
+        mass += visitable.getMass();
+        goDeep(visitable);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final BodyComponent visitable) {
+        mass += visitable.getMass();
+        goDeep(visitable);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final RingComponent visitable) {
+        mass += visitable.getMass();
+        goDeep(visitable);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final InnerTube visitable) {
+        mass += visitable.getMass();
+        goDeep(visitable);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final LaunchLug visitable) {
+        mass += visitable.getMass();
+        goDeep(visitable);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final Transition visitable) {
+        mass += visitable.getMass();
+        goDeep(visitable);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final RadiusRingComponent visitable) {
+        mass += visitable.getMass();
+        goDeep(visitable);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final NoseCone visitable) {
+        mass += visitable.getMass();
+        goDeep(visitable);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final BodyTube visitable) {
+        mass += visitable.getMass();
+        goDeep(visitable);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final TrapezoidFinSet visitable) {
+        mass += visitable.getMass();
+        goDeep(visitable);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final EllipticalFinSet visitable) {
+        mass += visitable.getMass();
+        goDeep(visitable);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void visit (final FreeformFinSet visitable) {
+        mass += visitable.getMass();
+        goDeep(visitable);
+    }
+
+    /**
+     * Get the list of stages, sort from Stage 1 .. Stage N.
+     *
+     * @return a sorted list of stages
+     */
+    public List<Double> getStages () {
+        return stageComponents;
+    }
+
+    /**
+     * Close by setting the last stage.
+     */
+    public void close () {
+        if (mass > 0d) {
+            stageComponents.add(mass);
+        }
+    }
+
+}
index 9cef22d13741d945a8c8a6055703a806f4d6f787..aa299490d15b70c0595d15a2818d5c53665bfb96 100644 (file)
@@ -1,12 +1,12 @@
 package net.sf.openrocket.gui.rocketfigure;
 
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.Transformation;
+
 import java.awt.Shape;
 import java.awt.geom.Ellipse2D;
 import java.awt.geom.Rectangle2D;
 
-import net.sf.openrocket.util.Coordinate;
-import net.sf.openrocket.util.Transformation;
-
 
 public class BodyTubeShapes extends RocketComponentShapes {
        
@@ -15,7 +15,7 @@ public class BodyTubeShapes extends RocketComponentShapes {
                net.sf.openrocket.rocketcomponent.BodyTube tube = (net.sf.openrocket.rocketcomponent.BodyTube)component;
 
                double length = tube.getLength();
-               double radius = tube.getRadius();
+               double radius = tube.getOuterRadius();
                Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0)));
 
                Shape[] s = new Shape[start.length];
@@ -31,7 +31,7 @@ public class BodyTubeShapes extends RocketComponentShapes {
                        Transformation transformation) {
                net.sf.openrocket.rocketcomponent.BodyTube tube = (net.sf.openrocket.rocketcomponent.BodyTube)component;
                
-               double or = tube.getRadius();
+               double or = tube.getOuterRadius();
                
                Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0)));
 
index 7dfb1b7a84d20ca0bce4fe9b078328b9b48a71e2..f7084ee6c49eb4116a674455ad82c7555b57bbef 100644 (file)
@@ -1,12 +1,12 @@
 package net.sf.openrocket.gui.rocketfigure;
 
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.Transformation;
+
 import java.awt.Shape;
 import java.awt.geom.Ellipse2D;
 import java.awt.geom.Rectangle2D;
 
-import net.sf.openrocket.util.Coordinate;
-import net.sf.openrocket.util.Transformation;
-
 
 public class LaunchLugShapes extends RocketComponentShapes {
        
@@ -15,7 +15,7 @@ public class LaunchLugShapes extends RocketComponentShapes {
                net.sf.openrocket.rocketcomponent.LaunchLug lug = (net.sf.openrocket.rocketcomponent.LaunchLug)component;
 
                double length = lug.getLength();
-               double radius = lug.getRadius();
+               double radius = lug.getOuterRadius();
                Coordinate[] start = transformation.transform(lug.toAbsolute(new Coordinate(0,0,0)));
 
                Shape[] s = new Shape[start.length];
@@ -31,7 +31,7 @@ public class LaunchLugShapes extends RocketComponentShapes {
                        Transformation transformation) {
                net.sf.openrocket.rocketcomponent.LaunchLug lug = (net.sf.openrocket.rocketcomponent.LaunchLug)component;
                
-               double or = lug.getRadius();
+               double or = lug.getOuterRadius();
                
                Coordinate[] start = transformation.transform(lug.toAbsolute(new Coordinate(0,0,0)));
 
index c19b5e8fe8403236bdbe1f253a0411ae0d37bf48..ea1f98c7180c4bf3d705f1c86d8c70b737a218a2 100644 (file)
@@ -1,6 +1,20 @@
 package net.sf.openrocket.gui.scalefigure;
 
 
+import net.sf.openrocket.gui.figureelements.FigureElement;
+import net.sf.openrocket.gui.main.ExceptionHandler;
+import net.sf.openrocket.motor.Motor;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.util.BugException;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.LineStyle;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Prefs;
+import net.sf.openrocket.util.Reflection;
+import net.sf.openrocket.util.Transformation;
+
 import java.awt.BasicStroke;
 import java.awt.Color;
 import java.awt.Dimension;
@@ -19,20 +33,6 @@ import java.util.Collection;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 
-import net.sf.openrocket.gui.figureelements.FigureElement;
-import net.sf.openrocket.gui.main.ExceptionHandler;
-import net.sf.openrocket.motor.Motor;
-import net.sf.openrocket.rocketcomponent.Configuration;
-import net.sf.openrocket.rocketcomponent.MotorMount;
-import net.sf.openrocket.rocketcomponent.RocketComponent;
-import net.sf.openrocket.util.BugException;
-import net.sf.openrocket.util.Coordinate;
-import net.sf.openrocket.util.LineStyle;
-import net.sf.openrocket.util.MathUtil;
-import net.sf.openrocket.util.Prefs;
-import net.sf.openrocket.util.Reflection;
-import net.sf.openrocket.util.Transformation;
-
 /**
  * A <code>ScaleFigure</code> that draws a complete rocket.  Extra information can
  * be added to the figure by the methods {@link #addRelativeExtra(FigureElement)},
@@ -256,14 +256,10 @@ public class RocketFigure extends AbstractScaleFigure {
                                tx = BORDER_PIXELS_WIDTH - minX*scale;
                        
                }
-               
-               if (figureHeightPx + 2*BORDER_PIXELS_HEIGHT < getHeight()) {
-                        ty = getHeight()/2;
-               } else {
-                        ty = BORDER_PIXELS_HEIGHT + figureHeightPx/2;
-               }
-               
-               if (Math.abs(translateX - tx)>1 || Math.abs(translateY - ty)>1) {
+
+        ty = computeTy(figureHeightPx);
+
+        if (Math.abs(translateX - tx)>1 || Math.abs(translateY - ty)>1) {
                        // Origin has changed, fire event
                        translateX = tx;
                        translateY = ty;
@@ -394,9 +390,19 @@ public class RocketFigure extends AbstractScaleFigure {
                }
 
        }
-       
-       
-       public RocketComponent[] getComponentsByPoint(double x, double y) {
+
+    protected double computeTy (int heightPx) {
+        final double ty;
+        if (heightPx + 2*BORDER_PIXELS_HEIGHT < getHeight()) {
+             ty = getHeight()/2;
+        } else {
+             ty = BORDER_PIXELS_HEIGHT + heightPx/2;
+        }
+        return ty;
+    }
+
+
+    public RocketComponent[] getComponentsByPoint(double x, double y) {
                // Calculate point in shapes' coordinates
                Point2D.Double p = new Point2D.Double(x,y);
                try {
index 6a8e68d035948d53c95129f4e8fd0da6577f3a12..c75fa072d4ff04ee795dd87c28cb977bf54d3897 100644 (file)
@@ -1,35 +1,6 @@
 package net.sf.openrocket.gui.scalefigure;
 
 
-import java.awt.Dimension;
-import java.awt.Font;
-import java.awt.Point;
-import java.awt.event.ActionEvent;
-import java.awt.event.InputEvent;
-import java.awt.event.MouseEvent;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
-
-import javax.swing.AbstractAction;
-import javax.swing.Action;
-import javax.swing.JComboBox;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JSlider;
-import javax.swing.JToggleButton;
-import javax.swing.JViewport;
-import javax.swing.SwingUtilities;
-import javax.swing.event.ChangeEvent;
-import javax.swing.event.ChangeListener;
-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;
@@ -67,6 +38,34 @@ import net.sf.openrocket.util.Coordinate;
 import net.sf.openrocket.util.MathUtil;
 import net.sf.openrocket.util.Prefs;
 
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+import javax.swing.JToggleButton;
+import javax.swing.JViewport;
+import javax.swing.SwingUtilities;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.tree.TreePath;
+import javax.swing.tree.TreeSelectionModel;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Point;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
 /**
  * A JPanel that contains a RocketFigure and buttons to manipulate the figure. 
  * 
@@ -260,8 +259,35 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change
        public Configuration getConfiguration() {
                return configuration;
        }
-       
-       public void setSelectionModel(TreeSelectionModel m) {
+
+    /**
+     * 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);
                }
index f17bd3d9f6c2152426c6c15d7632782b537a0322..9eb970c610ab8c2f1c11b93d27dfe54861cea4ec 100644 (file)
@@ -23,7 +23,6 @@ public abstract class BodyComponent extends ExternalComponent {
        }
        
        
-       
        /**
         * Get the outer radius of the component at cylindrical coordinate (x,theta).
         * 
@@ -76,4 +75,16 @@ public abstract class BodyComponent extends ExternalComponent {
                        return true;
                return false;
        }
+    
+    /**
+     * Accept a visitor to this BodyComponent in the component hierarchy.
+     * 
+     * @param theVisitor  the visitor that will be called back with a reference to this BodyComponent
+     */
+    @Override 
+    public void accept (final ComponentVisitor theVisitor) {
+        theVisitor.visit(this);
+    }
+    
+    
 }
index 2e272f95e097da9dc0430406ccde01a3d6d11529..fd5eb4a48cb995a79f7733a720deebe27b43a1ce 100644 (file)
@@ -1,13 +1,13 @@
 package net.sf.openrocket.rocketcomponent;
 
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-
 import net.sf.openrocket.motor.Motor;
 import net.sf.openrocket.util.Coordinate;
 import net.sf.openrocket.util.MathUtil;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+
 
 /**
  * Rocket body tube component.  Has only two parameters, a radius and length.
@@ -15,7 +15,7 @@ import net.sf.openrocket.util.MathUtil;
  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
  */
 
-public class BodyTube extends SymmetricComponent implements MotorMount {
+public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial {
 
        private double radius=0;
        private boolean autoRadius = false;  // Radius chosen automatically based on parent component
@@ -61,8 +61,11 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
 
        /**
         * Return the outer radius of the body tube.
+     * 
+     * @return  the outside radius of the tube
         */
-       public double getRadius() {
+    @Override
+       public double getOuterRadius () {
                if (autoRadius) {
                        // Return auto radius from front or rear
                        double r = -1;
@@ -88,8 +91,11 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
         * Set the outer radius of the body tube.  If the radius is less than the wall thickness,
         * the wall thickness is decreased accordingly of the value of the radius.
         * This method sets the automatic radius off.
+     * 
+     * @param radius  the outside radius in standard units
         */
-       public void setRadius(double radius) {
+    @Override
+       public void setOuterRadius (double radius) {
                if ((this.radius == radius) && (autoRadius==false))
                        return;
                
@@ -123,9 +129,9 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
        
        
        @Override
-       public double getAftRadius() {  return getRadius(); }
+       public double getAftRadius() {  return getOuterRadius(); }
        @Override
-       public double getForeRadius() { return getRadius(); }
+       public double getForeRadius() { return getOuterRadius(); }
        @Override
        public boolean isAftRadiusAutomatic() { return isRadiusAutomatic();     }
        @Override
@@ -144,7 +150,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
                                return -1;
                        }
                }
-               return getRadius();
+               return getOuterRadius();
        }
 
        @Override
@@ -158,7 +164,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
                                return -1;
                        }
                }
-               return getRadius();
+               return getOuterRadius();
        }
 
 
@@ -166,15 +172,16 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
        
        
        
-       
+    @Override  
        public double getInnerRadius() {
                if (filled)
                        return 0;
-               return Math.max(getRadius()-thickness, 0);
+               return Math.max(getOuterRadius()-thickness, 0);
        }
        
+    @Override
        public void setInnerRadius(double r) {
-               setThickness(getRadius()-r);
+               setThickness(getOuterRadius()-r);
        }
        
        
@@ -188,16 +195,26 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
                return "Body tube";
        }
 
+    /**
+     * Accept a visitor to this BodyTube in the component hierarchy.
+     * 
+     * @param theVisitor  the visitor that will be called back with a reference to this BodyTube
+     */
+    @Override 
+    public void accept (final ComponentVisitor theVisitor) {
+        theVisitor.visit(this);
+    }
+
 
        /************ Component calculations ***********/
        
        // From SymmetricComponent
        /**
-        * Returns the outer radius at the position x.  This returns the same value as getRadius().
+        * Returns the outer radius at the position x.  This returns the same value as getOuterRadius().
         */
        @Override
        public double getRadius(double x) {
-               return getRadius();
+               return getOuterRadius();
        }
 
        /**
@@ -208,7 +225,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
                if (filled)
                        return 0.0;
                else
-                       return Math.max(getRadius()-thickness,0);
+                       return Math.max(getOuterRadius()-thickness,0);
        }
        
 
@@ -225,7 +242,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
         */
        @Override
        public double getComponentVolume() {
-               double r = getRadius();
+               double r = getOuterRadius();
                if (filled)
                        return getFilledVolume(r,length);
                else
@@ -236,14 +253,14 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
        @Override
        public double getLongitudalUnitInertia() {
                // 1/12 * (3 * (r1^2 + r2^2) + h^2)
-               return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getRadius()) +
+               return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getOuterRadius()) +
                                MathUtil.pow2(getLength())) / 12;
        }
 
        @Override
        public double getRotationalUnitInertia() {
                // 1/2 * (r1^2 + r2^2)
-               return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getRadius()))/2;
+               return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getOuterRadius()))/2;
        }
 
 
@@ -266,7 +283,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
        @Override
        public Collection<Coordinate> getComponentBounds() {
                Collection<Coordinate> bounds = new ArrayList<Coordinate>(8);
-               double r = getRadius();
+               double r = getOuterRadius();
                addBound(bounds,0,r);
                addBound(bounds,length,r);
                return bounds;
index b0f2aa931ce3ab14f3c96638ee5f4e769b7c06ac..1621042c6551c61c928b759383d2513e88c20e24 100644 (file)
@@ -61,5 +61,15 @@ public class CenteringRing extends RadiusRingComponent {
        public boolean isCompatible(Class<? extends RocketComponent> type) {
                return false;
        }
+    
+    /**
+     * Accept a visitor to this CenteringRing in the component hierarchy.
+     * 
+     * @param theVisitor  the visitor that will be called back with a reference to this CenteringRing
+     */
+    @Override 
+    public void accept (final ComponentVisitor theVisitor) {
+        theVisitor.visit(this);
+    }
 
 }
diff --git a/src/net/sf/openrocket/rocketcomponent/Coaxial.java b/src/net/sf/openrocket/rocketcomponent/Coaxial.java
new file mode 100644 (file)
index 0000000..ad2338f
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * Coaxial.java
+ */
+package net.sf.openrocket.rocketcomponent;
+
+/**
+ * This interface defines the API for components that are axially
+ * symmetric.  It differs from RadialParent in that RadialParent applies
+ * to axially symmetric components whose radius varies with position, while
+ * this interface is for components that have a constant radius over it's length.
+ */
+public interface Coaxial {
+
+    /**
+     * Get the length of the radius of the inside dimension, in standard units.
+     * 
+     * @return the inner radius
+     */
+    double getInnerRadius();
+
+    /**
+     * Set the length of the radius of the inside dimension, in standard units.
+     * 
+     * @param v  the length of the inner radius
+     */
+    void setInnerRadius(double v);
+    
+    /**
+     * Get the length of the radius of the outside dimension, in standard units.
+     * 
+     * @return the outer radius
+     */
+    double getOuterRadius();
+    
+    /**
+     * Set the length of the radius of the outside dimension, in standard units.
+     * 
+     * @param v  the length of the outer radius
+     */
+    void setOuterRadius(double v);
+
+    /**
+     * Get the wall thickness of the component.  Typically this is just
+     * the outer radius - inner radius.
+     * 
+     * @return  the thickness of the wall
+     */
+    double getThickness();
+    
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/ComponentVisitor.java b/src/net/sf/openrocket/rocketcomponent/ComponentVisitor.java
new file mode 100644 (file)
index 0000000..636f5f0
--- /dev/null
@@ -0,0 +1,169 @@
+/*
+ * ComponentVisitor.java
+ */
+package net.sf.openrocket.rocketcomponent;
+
+/**
+ * This class implements a Visitor pattern to visit any/all components of a Rocket.
+ */
+public class ComponentVisitor implements Visitor<ComponentVisitor, RocketComponent> {
+
+    /**
+     * The delegate.
+     */
+    private ComponentVisitorStrategy strategy;
+
+    /**
+     * Constructor.
+     * 
+     * @param aStrategy  the object to delegate the visiting to
+     */
+    public ComponentVisitor (ComponentVisitorStrategy aStrategy) {
+        strategy = aStrategy;
+        strategy.setParent(this);
+    }
+
+    /**
+     * Visit a Rocket object.
+     * 
+     * @param visitable  the Rocket to visit
+     */
+    public void visit (final Rocket visitable) {
+        strategy.visit(visitable);
+    }
+
+    /**
+     * Visit a RocketComponent object.  This is used to catch any RocketComponent subclass not explicity
+     * defined by a visit method in this strategy.  
+     * 
+     * @param visitable  the RocketComponent to visit
+     */
+    public void visit (final RocketComponent visitable) {
+        strategy.visit(visitable);
+    }
+
+    /**
+     * Visit a Stage object.
+     * 
+     * @param visitable  the Stage to visit
+     */
+    public void visit (final Stage visitable) {
+        strategy.visit(visitable);
+    }
+
+    /**
+     * Visit an ExternalComponent object.
+     * 
+     * @param visitable  the ExternalComponent to visit
+     */
+    public void visit (final ExternalComponent visitable) {
+        strategy.visit(visitable);
+    }
+
+    /**
+     * Visit a BodyComponent object.
+     * 
+     * @param visitable  the BodyComponent to visit
+     */
+    public void visit (final BodyComponent visitable) {
+        strategy.visit(visitable);
+    }
+
+    /**
+     * Visit a RingComponent object.
+     * 
+     * @param visitable  the RingComponent to visit
+     */
+    public void visit (final RingComponent visitable) {
+        strategy.visit(visitable);
+    }
+
+    /**
+     * Visit an InnerTube object.
+     * 
+     * @param visitable  the InnerTube to visit
+     */
+    public void visit (final InnerTube visitable) {
+        strategy.visit(visitable);
+    }
+
+    /**
+     * Visit a LaunchLug object.
+     * 
+     * @param visitable  the LaunchLug to visit
+     */
+    public void visit (final LaunchLug visitable) {
+        strategy.visit(visitable);    
+    }
+    
+    /**
+     * Visit a Transition object.
+     * 
+     * @param visitable  the Transition to visit
+     */
+    public void visit (final Transition visitable) {
+        strategy.visit(visitable);
+    }
+
+    /**
+     * Visit a RadiusRingComponent object.
+     * 
+     * @param visitable  the RadiusRingComponent to visit
+     */
+    public void visit (final RadiusRingComponent visitable) {
+        strategy.visit(visitable);
+    }
+
+    /**
+     * Visit a NoseCone object.
+     * 
+     * @param visitable  the NoseCone to visit
+     */
+    public void visit (final NoseCone visitable) {
+        strategy.visit(visitable);
+    }
+
+    /**
+     * Visit a BodyTube object.
+     * 
+     * @param visitable  the BodyTube to visit
+     */
+    public void visit (final BodyTube visitable) {
+        strategy.visit(visitable);
+    }
+
+    /**
+     * Visit a Rocket object.
+     * 
+     * @param visitable  the Rocket to visit
+     */
+    public void visit (final TrapezoidFinSet visitable) {
+        strategy.visit(visitable);
+    }
+
+    /**
+     * Visit a Rocket object.
+     * 
+     * @param visitable  the Rocket to visit
+     */
+    public void visit (final EllipticalFinSet visitable) {
+        strategy.visit(visitable);
+    }
+
+    /**
+     * Visit a FreeformFinSet object.
+     * 
+     * @param visitable  the FreeformFinSet to visit
+     */
+    public void visit (final FreeformFinSet visitable) {
+        strategy.visit(visitable);
+    }
+
+    /**
+     * Perform any cleanup or finishing operations.
+     */
+    public void close () {
+        strategy.close();
+    }
+
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/ComponentVisitorStrategy.java b/src/net/sf/openrocket/rocketcomponent/ComponentVisitorStrategy.java
new file mode 100644 (file)
index 0000000..5956723
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+ * ComponentVisitorStrategy.java
+ */
+package net.sf.openrocket.rocketcomponent;
+
+/**
+ * This interface defines the methods used in a Rocket component visitor.  By using a strategy, 
+ * we can reuse one visitor definition and just instrument it with different strategies.  
+ */
+public interface ComponentVisitorStrategy {
+
+    /**
+     * Visit a Rocket object.
+     * 
+     * @param visitable  the Rocket to visit
+     */
+    void visit (final Rocket visitable);
+
+    /**
+     * Visit a RocketComponent object.  This is used to catch any RocketComponent subclass not explicity
+     * defined by a visit method in this strategy.  
+     * 
+     * @param visitable  the RocketComponent to visit
+     */
+    void visit (final RocketComponent visitable);
+
+    /**
+     * Visit a Stage object.
+     * 
+     * @param visitable  the Stage to visit
+     */
+    void visit (final Stage visitable);
+
+    /**
+     * Visit an ExternalComponent object.
+     * 
+     * @param visitable  the ExternalComponent to visit
+     */
+    void visit (final ExternalComponent visitable);
+
+    /**
+     * Visit a BodyComponent object.
+     * 
+     * @param visitable  the BodyComponent to visit
+     */
+    void visit (final BodyComponent visitable);
+
+    /**
+     * Visit a RingComponent object.
+     * 
+     * @param visitable  the RingComponent to visit
+     */
+    void visit (final RingComponent visitable);
+
+    /**
+     * Visit an InnerTube object.
+     * 
+     * @param visitable  the InnerTube to visit
+     */
+    void visit (final InnerTube visitable);
+
+    /**
+     * Visit a LaunchLug object.
+     * 
+     * @param visitable  the LaunchLug to visit
+     */
+    void visit (final LaunchLug visitable);
+    
+    /**
+     * Visit a Transition object.
+     * 
+     * @param visitable  the Transition to visit
+     */
+    void visit (final Transition visitable);
+
+    /**
+     * Visit a RadiusRingComponent object.
+     * 
+     * @param visitable  the RadiusRingComponent to visit
+     */
+    void visit (final RadiusRingComponent visitable);
+
+    /**
+     * Visit a NoseCone object.
+     * 
+     * @param visitable  the NoseCone to visit
+     */
+    void visit (final NoseCone visitable);
+
+    /**
+     * Visit a BodyTube object.
+     * 
+     * @param visitable  the BodyTube to visit
+     */
+    void visit (final BodyTube visitable);
+
+    /**
+     * Visit a TrapezoidFinSet object.
+     * 
+     * @param visitable  the TrapezoidFinSet to visit
+     */
+    void visit (final TrapezoidFinSet visitable);
+
+    /**
+     * Visit an EllipticalFinSet object.
+     * 
+     * @param visitable  the EllipticalFinSet to visit
+     */
+    void visit (final EllipticalFinSet visitable);
+
+    /**
+     * Visit a FreeformFinSet object.
+     * 
+     * @param visitable  the FreeformFinSet to visit
+     */
+    void visit (final FreeformFinSet visitable);
+
+    /**
+     * Set the visitor that is using this strategy.
+     * 
+     * @param parent  the visitor
+     */
+    void setParent(ComponentVisitor parent);
+
+    /**
+     * Perform any cleanup or finishing operations.
+     */
+    void close();
+}
index d4ecac8319b2b9600dabde5b45b5e94cd7912ca2..22d49bea65f2850b38d68d52953c9218ec17afd3 100644 (file)
@@ -67,4 +67,15 @@ public class EllipticalFinSet extends FinSet {
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
 
+    
+    /**
+     * Accept a visitor to this EllipticalFinSet in the component hierarchy.
+     * 
+     * @param theVisitor  the visitor that will be called back with a reference to this EllipticalFinSet
+     */
+    @Override
+    public void accept(ComponentVisitor theVisitor) {
+        theVisitor.visit(this);
+    }
+    
 }
index 8e11c9140eefa1bbee588d3b4f97c663d7c247f8..8a12090465f764ce457652df8117fb8f9708c68e 100644 (file)
@@ -126,4 +126,14 @@ public abstract class ExternalComponent extends RocketComponent {
                this.material = src.material;
        }
        
+    /**
+     * Accept a visitor to this ExternalComponent in the component hierarchy.
+     * 
+     * @param theVisitor  the visitor that will be called back with a reference to this ExternalComponent
+     */
+    @Override 
+    public void accept (final ComponentVisitor theVisitor) {
+        theVisitor.visit(this);
+    }
+    
 }
index 19f9e0ada6e4be564a0458358893f0f70b731a78..1b80e42641fb167faf391355440ec4862fbf39b7 100644 (file)
@@ -1,11 +1,11 @@
 package net.sf.openrocket.rocketcomponent;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-
 import net.sf.openrocket.util.BugException;
 import net.sf.openrocket.util.Coordinate;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+
 
 public class FreeformFinSet extends FinSet {
 
@@ -305,6 +305,15 @@ public class FreeformFinSet extends FinSet {
                return c;
        }
        
+    /**
+     * Accept a visitor to this FreeformFinSet in the component hierarchy.
+     * 
+     * @param theVisitor  the visitor that will be called back with a reference to this FreeformFinSet
+     */    
+    @Override
+    public void accept(ComponentVisitor theVisitor) {
+        theVisitor.visit(this);
+    }
        
        private void validate(ArrayList<Coordinate> points) throws IllegalFinPointException {
                final int n = points.size();
index 8ef7d1f7bb9bf3b9250256cbebaddbec98834a06..0a0b6327fdfd9f7b86238ca2304955c77ceeb8be 100644 (file)
@@ -1,13 +1,13 @@
 package net.sf.openrocket.rocketcomponent;
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-
 import net.sf.openrocket.motor.Motor;
 import net.sf.openrocket.util.Coordinate;
 import net.sf.openrocket.util.MathUtil;
 
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
 
 /**
  * This class defines an inner tube that can be used as a motor mount.  The component
@@ -307,6 +307,15 @@ implements Clusterable, RadialParent, MotorMount {
        }
 
        
+    /**
+     * Accept a visitor to an InnerTube object in the component hierarchy.
+     * 
+     * @param theVisitor  the visitor that will be called back with a reference to this InnerTube
+     */
+    @Override 
+    public void accept (final ComponentVisitor theVisitor) {
+        theVisitor.visit(this);
+    }
        
        
        /*
index 9fbfcae4856706fc68b963a7f35975bc7e21f24a..2670bc6063741d925b82dbd47926bb3b4df69d4d 100644 (file)
@@ -1,13 +1,13 @@
 package net.sf.openrocket.rocketcomponent;
 
-import java.util.ArrayList;
-import java.util.Collection;
-
 import net.sf.openrocket.util.Coordinate;
 import net.sf.openrocket.util.MathUtil;
 
+import java.util.ArrayList;
+import java.util.Collection;
+
 
-public class LaunchLug extends ExternalComponent {
+public class LaunchLug extends ExternalComponent implements Coaxial {
 
        private double radius;
        private double thickness;
@@ -27,11 +27,11 @@ public class LaunchLug extends ExternalComponent {
        }
        
        
-       public double getRadius() {
+       public double getOuterRadius () {
                return radius;
        }
 
-       public void setRadius(double radius) {
+       public void setOuterRadius (double radius) {
                if (MathUtil.equals(this.radius, radius))
                        return;
                this.radius = radius;
@@ -44,7 +44,7 @@ public class LaunchLug extends ExternalComponent {
        }
 
        public void setInnerRadius(double innerRadius) {
-               setRadius(innerRadius + thickness);
+               setOuterRadius(innerRadius + thickness);
        }
 
        public double getThickness() {
@@ -175,14 +175,14 @@ public class LaunchLug extends ExternalComponent {
        @Override
        public double getLongitudalUnitInertia() {
                // 1/12 * (3 * (r1^2 + r2^2) + h^2)
-               return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getRadius()) +
+               return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getOuterRadius()) +
                                MathUtil.pow2(getLength())) / 12;
        }
 
        @Override
        public double getRotationalUnitInertia() {
                // 1/2 * (r1^2 + r2^2)
-               return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getRadius()))/2;
+               return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getOuterRadius()))/2;
        }
 
 
@@ -192,4 +192,14 @@ public class LaunchLug extends ExternalComponent {
                return false;
        }
 
+    /**
+     * Accept a visitor to this LaunchLug in the component hierarchy.
+     * 
+     * @param theVisitor  the visitor that will be called back with a reference to this LaunchLug
+     */    
+    @Override 
+    public void accept (final ComponentVisitor theVisitor) {
+        theVisitor.visit(this);
+    }
+    
 }
index 04eaa77c8b5c70ce448a0c9b60d7e6932689f17f..1aa61a4cb38d297615cda765f4f0e5c4ffaebbe0 100644 (file)
@@ -114,4 +114,14 @@ public class NoseCone extends Transition {
        public String getComponentName() {
                return "Nose cone";
        }
+    
+    /**
+     * Accept a visitor to this NoseCone in the component hierarchy.
+     * 
+     * @param theVisitor  the visitor that will be called back with a reference to this NoseCone
+     */
+    @Override
+    public void accept(ComponentVisitor theVisitor) {
+        theVisitor.visit(this);
+    }
 }
index f7b7be84d84bc6674e65b2553fddeeb7607f69e0..b3d862510c99505c3b7b81ca35f349eef8446fa9 100644 (file)
@@ -11,7 +11,7 @@ import net.sf.openrocket.util.MathUtil;
  * 
  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
  */
-public abstract class RadiusRingComponent extends RingComponent {
+public abstract class RadiusRingComponent extends RingComponent implements Coaxial {
 
        protected double outerRadius = 0;
        protected double innerRadius = 0;
@@ -80,4 +80,15 @@ public abstract class RadiusRingComponent extends RingComponent {
                thickness = MathUtil.clamp(thickness, 0, outer);
                setInnerRadius(outer - thickness);
        }
+
+    /**
+     * Accept a visitor to this RadiusRingComponent in the component hierarchy.
+     * 
+     * @param theVisitor  the visitor that will be called back with a reference to this RadiusRingComponent
+     */    
+    @Override 
+    public void accept (final ComponentVisitor theVisitor) {
+        theVisitor.visit(this);
+    }
+    
 }
index 3d67940b6728650c99580d78af786ee142c47343..0450b4155addb667d212f6b8feee57851b33a510 100644 (file)
@@ -1,12 +1,12 @@
 package net.sf.openrocket.rocketcomponent;
 
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 
-import net.sf.openrocket.util.Coordinate;
-import net.sf.openrocket.util.MathUtil;
-
 
 /**
  * An inner component that consists of a hollow cylindrical component.  This can be
@@ -16,7 +16,7 @@ import net.sf.openrocket.util.MathUtil;
  * 
  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
  */
-public abstract class RingComponent extends StructuralComponent {
+public abstract class RingComponent extends StructuralComponent implements Coaxial {
 
        protected boolean outerRadiusAutomatic = false;
        protected boolean innerRadiusAutomatic = false;
@@ -30,13 +30,17 @@ public abstract class RingComponent extends StructuralComponent {
        
 
        
-
+    @Override
        public abstract double getOuterRadius();
+    @Override
        public abstract void setOuterRadius(double r);
        
+    @Override
        public abstract double getInnerRadius();        
+    @Override
        public abstract void setInnerRadius(double r);
        
+    @Override
        public abstract double getThickness();
        public abstract void setThickness(double thickness);
        
@@ -209,4 +213,14 @@ public abstract class RingComponent extends StructuralComponent {
                return ringRotationalUnitInertia(getOuterRadius(), getInnerRadius());
        }
 
+    /**
+     * Accept a visitor to this RingComponent in the component hierarchy.
+     * 
+     * @param theVisitor  the visitor that will be called back with a reference to this RingComponent
+     */    
+    @Override 
+    public void accept (final ComponentVisitor theVisitor) {
+        theVisitor.visit(this);
+    }
+
 }
index 9ab110b49c75996e2a5efb337638d980a0cf43ef..a0c923ed2592219aaa14c0b4264cb4bf63db0b05 100644 (file)
@@ -1,5 +1,12 @@
 package net.sf.openrocket.rocketcomponent;
 
+import net.sf.openrocket.motor.Motor;
+import net.sf.openrocket.util.Chars;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+import javax.swing.event.ChangeListener;
+import javax.swing.event.EventListenerList;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -9,14 +16,6 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.UUID;
 
-import javax.swing.event.ChangeListener;
-import javax.swing.event.EventListenerList;
-
-import net.sf.openrocket.motor.Motor;
-import net.sf.openrocket.util.Chars;
-import net.sf.openrocket.util.Coordinate;
-import net.sf.openrocket.util.MathUtil;
-
 
 /**
  * Base for all rocket components.  This is the "starting point" for all rocket trees.
@@ -135,7 +134,6 @@ public class Rocket extends RocketComponent {
        }
        
        
-       
        /**
         * Return the non-negative modification ID of this rocket.  The ID is changed
         * every time any change occurs in the rocket.  This can be used to check 
@@ -795,4 +793,14 @@ public class Rocket extends RocketComponent {
        public boolean isCompatible(Class<? extends RocketComponent> type) {
                return (Stage.class.isAssignableFrom(type));
        }
+
+    /**
+     * Accept a visitor to this Rocket in the component hierarchy.
+     * 
+     * @param theVisitor  the visitor that will be called back with a reference to this Rocket
+     */    
+       @Override 
+    public void accept (final ComponentVisitor theVisitor) {
+        theVisitor.visit(this);
+    }    
 }
index a945d7b2dd9ecf5a38d063c9a971a93f84216f79..71d2c1390821d122ab9c966b30b572313e025e2c 100644 (file)
@@ -1,5 +1,12 @@
 package net.sf.openrocket.rocketcomponent;
 
+import net.sf.openrocket.util.BugException;
+import net.sf.openrocket.util.ChangeSource;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.LineStyle;
+import net.sf.openrocket.util.MathUtil;
+
+import javax.swing.event.ChangeListener;
 import java.awt.Color;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -11,17 +18,9 @@ import java.util.NoSuchElementException;
 import java.util.Stack;
 import java.util.UUID;
 
-import javax.swing.event.ChangeListener;
-
-import net.sf.openrocket.util.BugException;
-import net.sf.openrocket.util.ChangeSource;
-import net.sf.openrocket.util.Coordinate;
-import net.sf.openrocket.util.LineStyle;
-import net.sf.openrocket.util.MathUtil;
-
 
 public abstract class RocketComponent implements ChangeSource, Cloneable, 
-               Iterable<RocketComponent> {
+               Iterable<RocketComponent> , Visitable<ComponentVisitor, RocketComponent> {
 
        /*
         * Text is suitable to the form
@@ -121,12 +120,8 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                this.relativePosition = relativePosition;
                this.id = UUID.randomUUID().toString();
        }
-       
-       
-       
-       
-       
-       ////////////  Methods that must be implemented  ////////////
+
+    ////////////  Methods that must be implemented  ////////////
 
 
        /**
@@ -328,6 +323,16 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
        }
 
 
+    /**
+     * Accept a visitor to this RocketComponent in the component hierarchy.
+     * 
+     * @param theVisitor  the visitor that will be called back with a reference to this RocketComponent
+     */
+    @Override 
+    public void accept (final ComponentVisitor theVisitor) {
+        theVisitor.visit(this);
+    }
+
        //////////////  Methods that may not be overridden  ////////////
        
        
index 74a36a5ebb9bd6b394d370bfbe304b6689a65fb2..ed2fc7a9129d5f20b69ffcf8b90fc12b7148a5e8 100644 (file)
@@ -2,22 +2,31 @@ package net.sf.openrocket.rocketcomponent;
 
 public class Stage extends ComponentAssembly {
 
-       @Override
-       public String getComponentName() {
-               return "Stage";
-       }
+    @Override
+    public String getComponentName () {
+        return "Stage";
+    }
 
-       
-       /**
-        * Check whether the given type can be added to this component.  A Stage allows 
-        * only BodyComponents to be added.
-        * 
-        * @param type  The RocketComponent class type to add.
-        * @return      Whether such a component can be added.
-        */
-       @Override
-       public boolean isCompatible(Class<? extends RocketComponent> type) {
-               return BodyComponent.class.isAssignableFrom(type);
-       }
 
+    /**
+     * Check whether the given type can be added to this component.  A Stage allows only BodyComponents to be added.
+     *
+     * @param type The RocketComponent class type to add.
+     *
+     * @return Whether such a component can be added.
+     */
+    @Override
+    public boolean isCompatible (Class<? extends RocketComponent> type) {
+        return BodyComponent.class.isAssignableFrom(type);
+    }
+
+    /**
+     * Accept a visitor to this Stage in the component hierarchy.
+     * 
+     * @param theVisitor  the visitor that will be called back with a reference to this Stage
+     */    
+    @Override 
+    public void accept (final ComponentVisitor theVisitor) {
+        theVisitor.visit(this);
+    }
 }
index f598ad1eeb6c5500a8be3305990f5c78245dbf44..f26666ae9079415722dedf1420f9419bd2b2fde6 100644 (file)
@@ -1,13 +1,16 @@
 package net.sf.openrocket.rocketcomponent;
 
-import static java.lang.Math.*;
-import static net.sf.openrocket.util.Chars.*;
-import static net.sf.openrocket.util.MathUtil.*;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
 
 import java.util.Collection;
 
-import net.sf.openrocket.util.Coordinate;
-import net.sf.openrocket.util.MathUtil;
+import static java.lang.Math.sin;
+import static java.lang.Math.sqrt;
+import static net.sf.openrocket.util.Chars.FRAC12;
+import static net.sf.openrocket.util.Chars.FRAC34;
+import static net.sf.openrocket.util.MathUtil.pow2;
+import static net.sf.openrocket.util.MathUtil.pow3;
 
 
 public class Transition extends SymmetricComponent {
@@ -354,7 +357,7 @@ public class Transition extends SymmetricComponent {
        /**
         * Numerically solve clipLength from the equation
         *     r1 == type.getRadius(clipLength,r2,clipLength+length)
-        * using a binary search.  It assumes getRadius() to be monotonically increasing.
+        * using a binary search.  It assumes getOuterRadius() to be monotonically increasing.
         */
        private void calculateClip(double r1, double r2) {
                double min=0, max=length;
@@ -497,6 +500,15 @@ public class Transition extends SymmetricComponent {
                clipLength = -1;
        }
        
+    /**
+     * Accept a visitor to this Transition in the component hierarchy.
+     * 
+     * @param theVisitor  the visitor that will be called back with a reference to this Transition
+     */
+    @Override 
+    public void accept (final ComponentVisitor theVisitor) {
+        theVisitor.visit(this);
+    }
        
        
        /**
index c92c65ea9b09c2aa691dfbce9e6f3f1d583e142a..6f8ef943dbf212ed755dc6ac30bc7c1fe327ec54 100644 (file)
@@ -166,4 +166,13 @@ public class TrapezoidFinSet extends FinSet {
                return "Trapezoidal fin set";
        }
 
+    /**
+     * Accept a visitor to this TrapezoidFinSet in the component hierarchy.
+     * 
+     * @param theVisitor  the visitor that will be called back with a reference to this TrapezoidFinSet
+     */    
+    @Override
+    public void accept(ComponentVisitor theVisitor) {
+        theVisitor.visit(this);
+    }
 }
diff --git a/src/net/sf/openrocket/rocketcomponent/Visitable.java b/src/net/sf/openrocket/rocketcomponent/Visitable.java
new file mode 100644 (file)
index 0000000..cb516a5
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * Visitable.java
+ */
+package net.sf.openrocket.rocketcomponent;
+
+/**
+ * This interface describes a portion of the Visitor pattern, using generics to assure type-safety.
+ * The elements of the concrete object hierarchy are only visitable by an associated hierarchy of visitors, 
+ * while these visitors are only able to visit the elements of that hierarchy. 
+ * 
+ * The key concept regarding the Visitor pattern is to realize that Java will only ÒdiscriminateÓ the type of an 
+ * object being called, not the type of an object being passed.
+ *
+ * In order for the type of two objects to be determinable to the JVM, each object must be the receiver of an
+ * invocation. Here, when accept is called on a Visitable, the concrete type of the Visitable becomes ÒknownÓ but the 
+ * concrete type of the argument is still unknown.  <code>visit</code> is then called on the parameter object, passing 
+ * the Visitable back, which has type and identity. Flow of control has now been 'double-dispatched' such that the 
+ * type (and identity) of both objects are known. 
+ * 
+ * Specifically, this interface is to be implemented by every class in the RocketComponent hierarchy that
+ * can be visited AND which are sufficiently specialized from their super class.  If they only provide 
+ * constraints to their superclass (such as TubeCoupler), then the implementation of this interface at 
+ * the superclass level is sufficient.
+ * 
+ * Admittedly, the syntax is a bit contorted here, as it is necessarily self-referential for type-safety.
+ * 
+ * <V> The visitor type
+ * <T> The visitable (the concrete class that implements this interface)
+ */
+public interface Visitable<V extends Visitor<V, T>, T extends Visitable<V, T>> {
+
+    /**
+     * Any class in the hierarchy that allows itself to be visited will implement this method.  The normal
+     * behavior is that the visitor will invoke this method of a Visitable, passing itself.  The Visitable 
+     * turns around calls the Visitor back. This idiom is also known as 'double-dispatching'.
+     * 
+     * @param visitor  the visitor that will be called back
+     */
+    public void accept(V visitor);
+
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/Visitor.java b/src/net/sf/openrocket/rocketcomponent/Visitor.java
new file mode 100644 (file)
index 0000000..2e689ea
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * Visitor.java
+ */
+package net.sf.openrocket.rocketcomponent;
+
+/**
+ * This interface describes a portion of the Visitor pattern, using generics to assure type-safety.
+ * The elements of the concrete object hierarchy are only visitable by an associated hierarchy of visitors, 
+ * while these visitors are only able to visit the elements of that hierarchy. 
+ * 
+ * The key concept regarding the Visitor pattern is to realize that Java will only ÒdiscriminateÓ the type of an 
+ * object being called, not the type of an object being passed.
+ *
+ * In order for the type of two objects to be determinable to the JVM, each object must be the receiver of an
+ * invocation. Here, when accept is called on a Visitable, the concrete type of the Visitable becomes ÒknownÓ but the 
+ * concrete type of the argument is still unknown.  <code>visit</code> is then called on the parameter object, passing 
+ * the Visitable back, which has type and identity. Flow of control has now been 'double-dispatched' such that the 
+ * type (and identity) of both objects are known. 
+ * 
+ * Specifically, this interface is to be implemented by every class in the RocketComponent hierarchy that
+ * can be visited AND which are sufficiently specialized from their super class.  If they only provide 
+ * constraints to their superclass (such as TubeCoupler), then the implementation of this interface at 
+ * the superclass level is sufficient.
+ * 
+ * Admittedly, the syntax is a bit contorted here, as it is necessarily self-referential for type-safety.
+ * 
+ * <V> The visitor type (the concrete class that implements this interface)
+ * <T> The visitable 
+ */
+public interface Visitor<V extends Visitor<V, T>, T extends Visitable<V, T>> {
+
+    /**
+     * The callback method.  This method is the 2nd leg of the double-dispatch, having been invoked from a 
+     * corresponding <code>accept</code>.
+     *           
+     * @param visitable  the instance of the Visitable (the target of what is being visiting)
+     */
+    void visit(T visitable);
+}
\ No newline at end of file
index 1748fcd8b3168d8e8aa0cdf8473a188d8da9a4b0..50d06547231250e0d5c471e69f20382d9c011110 100644 (file)
@@ -1,8 +1,5 @@
 package net.sf.openrocket.util;
 
-import java.awt.Color;
-import java.util.Random;
-
 import net.sf.openrocket.material.Material;
 import net.sf.openrocket.material.Material.Type;
 import net.sf.openrocket.motor.Motor;
@@ -10,27 +7,30 @@ import net.sf.openrocket.rocketcomponent.BodyTube;
 import net.sf.openrocket.rocketcomponent.Bulkhead;
 import net.sf.openrocket.rocketcomponent.CenteringRing;
 import net.sf.openrocket.rocketcomponent.ExternalComponent;
+import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish;
+import net.sf.openrocket.rocketcomponent.FinSet.CrossSection;
 import net.sf.openrocket.rocketcomponent.FreeformFinSet;
 import net.sf.openrocket.rocketcomponent.IllegalFinPointException;
 import net.sf.openrocket.rocketcomponent.InnerTube;
 import net.sf.openrocket.rocketcomponent.InternalComponent;
 import net.sf.openrocket.rocketcomponent.LaunchLug;
 import net.sf.openrocket.rocketcomponent.MassComponent;
+import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent;
 import net.sf.openrocket.rocketcomponent.NoseCone;
 import net.sf.openrocket.rocketcomponent.ReferenceType;
 import net.sf.openrocket.rocketcomponent.Rocket;
 import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.RocketComponent.Position;
 import net.sf.openrocket.rocketcomponent.Stage;
 import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.rocketcomponent.Transition.Shape;
 import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
 import net.sf.openrocket.rocketcomponent.TubeCoupler;
-import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish;
-import net.sf.openrocket.rocketcomponent.FinSet.CrossSection;
-import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent;
-import net.sf.openrocket.rocketcomponent.RocketComponent.Position;
-import net.sf.openrocket.rocketcomponent.Transition.Shape;
 import net.sf.openrocket.startup.Application;
 
+import java.awt.Color;
+import java.util.Random;
+
 public class TestRockets {
        
        private final String key;
@@ -139,7 +139,7 @@ public class TestRockets {
                body.setLength(rnd(0.3));
                body.setMotorMount(rnd.nextBoolean());
                body.setMotorOverhang(rnd.nextGaussian() * 0.03);
-               body.setRadius(rnd(0.06));
+               body.setOuterRadius(rnd(0.06));
                body.setRadiusAutomatic(rnd.nextBoolean());
                stage.addChild(body);
                
index 689c86f231560f3f3f387d241801d7ccf7f4f72d..9c6a48b973d0b51355dfe2c0d6c52293a28c8e6e 100644 (file)
@@ -107,11 +107,11 @@ public class LaunchLugHandlerTest extends RocksimTestBase {
         WarningSet warnings = new WarningSet();
 
         handler.closeElement("OD", attributes, "-1", warnings);
-        assertEquals(0d, component.getRadius());
+        assertEquals(0d, component.getOuterRadius());
         handler.closeElement("OD", attributes, "0", warnings);
-        assertEquals(0d, component.getRadius());
+        assertEquals(0d, component.getOuterRadius());
         handler.closeElement("OD", attributes, "75", warnings);
-        assertEquals(75d / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS, component.getRadius());
+        assertEquals(75d / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS, component.getOuterRadius());
         handler.closeElement("OD", attributes, "foo", warnings);
         assertEquals(1, warnings.size());
         warnings.clear();