]> git.gag.com Git - debian/openrocket/commitdiff
DGP - merged printing support from branch
authorrodinia814 <rodinia814@180e2498-e6e9-4542-8430-84ac67f01cd8>
Fri, 12 Nov 2010 02:51:18 +0000 (02:51 +0000)
committerrodinia814 <rodinia814@180e2498-e6e9-4542-8430-84ac67f01cd8>
Fri, 12 Nov 2010 02:51:18 +0000 (02:51 +0000)
git-svn-id: https://openrocket.svn.sourceforge.net/svnroot/openrocket/trunk@95 180e2498-e6e9-4542-8430-84ac67f01cd8

15 files changed:
1  2 
.classpath
src/net/sf/openrocket/file/rocksim/LaunchLugHandler.java
src/net/sf/openrocket/gui/main/BasicFrame.java
src/net/sf/openrocket/gui/print/DesignReport.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/FreeformFinSet.java
src/net/sf/openrocket/rocketcomponent/InnerTube.java
src/net/sf/openrocket/rocketcomponent/LaunchLug.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

diff --cc .classpath
Simple merge
index 4f1561cef5cb624e60d4c1dc567b9694a7133faf,43c214c72e5eb31e2ff95e22bec2162b72fcfea0..3c8f4e2c7e76ed02230525f04147421db1776323
@@@ -1,64 -1,10 +1,7 @@@
  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.net.URI;
- import java.net.URISyntaxException;
- import java.net.URL;
- import java.net.URLDecoder;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.LinkedList;
- import java.util.List;
- 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.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;
 -import net.sf.openrocket.communication.UpdateInfoRetriever;
 -import net.sf.openrocket.database.Databases;
  import net.sf.openrocket.document.OpenRocketDocument;
  import net.sf.openrocket.file.GeneralRocketLoader;
  import net.sf.openrocket.file.RocketLoadException;
@@@ -75,10 -19,11 +18,11 @@@ import net.sf.openrocket.gui.dialogs.De
  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;
  import net.sf.openrocket.gui.dialogs.preferences.PreferencesDialog;
 +import net.sf.openrocket.gui.main.componenttree.ComponentTree;
  import net.sf.openrocket.gui.scalefigure.RocketPanel;
  import net.sf.openrocket.logging.LogHelper;
  import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
@@@ -98,6 -41,62 +42,62 @@@ import net.sf.openrocket.util.Reflectio
  import net.sf.openrocket.util.SaveFileWorker;
  import net.sf.openrocket.util.TestRockets;
  
 -import javax.swing.Timer;
 -import javax.swing.ToolTipManager;
+ 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 java.lang.reflect.InvocationTargetException;
+ 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.net.URI;
+ import java.net.URISyntaxException;
+ import java.net.URL;
+ import java.net.URLDecoder;
+ import java.util.ArrayList;
++import java.util.Arrays;
++import java.util.LinkedList;
++import java.util.List;
+ import java.util.concurrent.ExecutionException;
  public class BasicFrame extends JFrame {
        private static final LogHelper log = Application.getLogger();
        
        }
        
        
 -      /**
 -       * Closes this frame if it is replaceable.
 -       */
 -      public void closeIfReplaceable() {
 -              if (this.replaceable && document.isSaved()) {
 -                      closeAction();
 -              }
 -      }
  
+     /**
+      * 
+      */
+     public void printAction() {
+         new PrintDialog(document);
+     }
+     
        /**
         * Open a new design window with a basic rocket+stage.
         */
index 0000000000000000000000000000000000000000,1da0bd00ed885e4a63536c27393548339c1d9fae..247881e8071f9b9f0050bec8a2d975cb45d762a1
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,466 +1,466 @@@
 - * DrawingsPrintable.java
+ /*
 -            Rocket duplicate = theRocket.copy();
++ * DesignReport.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
+      * @param stageWeights the stageWeights of each stage, in order
+      */
+     private void addMotorData (List<Motor> motors, final PdfPTable parent, List<Double> stageWeights) {
+         PdfPTable motorTable = new PdfPTable(8);
+         motorTable.setWidthPercentage(68);
+         motorTable.setHorizontalAlignment(Element.ALIGN_LEFT);
+         final PdfPCell motorCell = ITextHelper.createCell("Motor", PdfPCell.BOTTOM);
+         final int mPad = 10;
+         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 Wt", PdfPCell.BOTTOM));
+         motorTable.addCell(ITextHelper.createCell("Propellant Wt", PdfPCell.BOTTOM));
+         motorTable.addCell(ITextHelper.createCell("Size", 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);
+             double motorWeight = (motor.getLaunchCG().weight - motor.getEmptyCG().weight) * 1000;  //convert to grams
+             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(stageWeights, i) + (motor
+                     .getLaunchCG().weight * 9.80665));
+             motorTable.addCell(ITextHelper.createCell(df.format(ttw) + ":1", border));
+             motorTable.addCell(ITextHelper.createCell(df.format(motorWeight) + " " + UnitGroup.UNITS_MASS
+                     .getDefaultUnit().toString(), 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.copyWithOriginalID();
+             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();
+         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;
+     }
+ }
index 884893d559f90949d80a5ee0854f667ad12c3a31,9eb970c610ab8c2f1c11b93d27dfe54861cea4ec..97a12406f4e764d00cce6abf580b796f14d56b0e
@@@ -59,9 -58,33 +58,20 @@@ public abstract class BodyComponent ext
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
        
 -      
 -      /**
 -       * Check whether the given type can be added to this component.  BodyComponents allow any
 -       * InternalComponents or ExternalComponents, excluding 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) {
 -              if (InternalComponent.class.isAssignableFrom(type))
 -                      return true;
 -              if (ExternalComponent.class.isAssignableFrom(type) &&
 -                      !BodyComponent.class.isAssignableFrom(type))
 -                      return true;
 -              return false;
 +      public boolean allowsChildren() {
 +              return true;
        }
-       
+     
+     /**
+      * 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 086c9efced9d03f6bbd8011c46e565ec86e066a0,fd5eb4a48cb995a79f7733a720deebe27b43a1ce..5656c72b32557bcc73a092242a6949e9180688c6
@@@ -15,10 -15,10 +15,10 @@@ import java.util.HashMap
   * @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
 +      
 +      private double radius = 0;
 +      private boolean autoRadius = false; // Radius chosen automatically based on parent component
        
        // When changing the inner radius, thickness is modified
        
                this.thickness = thickness;
        }
        
 -
 +      
        /************  Get/set component parameter methods ************/
 -
 +      
        /**
         * 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;
         * 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))
 +              if ((this.radius == radius) && (autoRadius == false))
                        return;
                
                this.autoRadius = false;
        
        
        @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
 -      public boolean isForeRadiusAutomatic() { return isRadiusAutomatic(); }
 +      public boolean isAftRadiusAutomatic() {
 +              return isRadiusAutomatic();
 +      }
        
 +      @Override
 +      public boolean isForeRadiusAutomatic() {
 +              return isRadiusAutomatic();
 +      }
        
        
 +
        @Override
        protected double getFrontAutoRadius() {
                if (isRadiusAutomatic()) {
                                return -1;
                        }
                }
-               return getRadius();
+               return getOuterRadius();
        }
 -
 +      
        @Override
        protected double getRearAutoRadius() {
                if (isRadiusAutomatic()) {
                                return -1;
                        }
                }
-               return getRadius();
+               return getOuterRadius();
        }
 +      
 +      
  
  
+       
+       
+       
+       
+     @Override 
        public double getInnerRadius() {
                if (filled)
                        return 0;
         */
        @Override
        public double getRadius(double x) {
-               return getRadius();
+               return getOuterRadius();
        }
 -
 +      
        /**
         * Returns the inner radius at the position x.  If the tube is filled, returns always zero.
         */
                if (filled)
                        return 0.0;
                else
-                       return Math.max(getRadius() - thickness, 0);
+                       return Math.max(getOuterRadius()-thickness,0);
        }
        
 -
 +      
        /**
         * Returns the body tube's center of gravity.
         */
         */
        @Override
        public double getComponentVolume() {
-               double r = getRadius();
+               double r = getOuterRadius();
                if (filled)
 -                      return getFilledVolume(r,length);
 +                      return getFilledVolume(r, length);
                else
 -                      return getFilledVolume(r,length) - getFilledVolume(getInnerRadius(0),length);
 +                      return getFilledVolume(r, length) - getFilledVolume(getInnerRadius(0), length);
        }
        
        
        @Override
        public double getLongitudalUnitInertia() {
                // 1/12 * (3 * (r1^2 + r2^2) + h^2)
-               return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getRadius()) + MathUtil.pow2(getLength())) / 12;
+               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;
        }
 -
 -
 +      
        
  
 +
        /**
         * Helper function for cylinder volume.
         */
        @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);
 +              addBound(bounds, 0, r);
 +              addBound(bounds, length, r);
                return bounds;
        }
 +      
 +      
  
 -
 +      /**
 +       * Check whether the given type can be added to this component.  BodyTubes allow any
 +       * InternalComponents or ExternalComponents, excluding 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) {
 +              if (InternalComponent.class.isAssignableFrom(type))
 +                      return true;
 +              if (ExternalComponent.class.isAssignableFrom(type) &&
 +                              !BodyComponent.class.isAssignableFrom(type))
 +                      return true;
 +              return false;
 +      }
 +      
        ////////////////  Motor mount  /////////////////
        
        @Override
index f51f4650fabe7f818dc972bdf927b0e1b21e2727,1b80e42641fb167faf391355440ec4862fbf39b7..ce6402151bc646972bd055b34235c9e63f4cc378
@@@ -1,17 -1,14 +1,17 @@@
  package net.sf.openrocket.rocketcomponent;
  
- import java.util.ArrayList;
- import java.util.Arrays;
 +import net.sf.openrocket.logging.LogHelper;
 +import net.sf.openrocket.startup.Application;
  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 {
 -
 +      private static final LogHelper log = Application.getLogger();
 +      
        private ArrayList<Coordinate> points = new ArrayList<Coordinate>();
        
        public FreeformFinSet() {
index 10c0e7ee9acb726c29ab78ee0c15b5c1a739367c,0a0b6327fdfd9f7b86238ca2304955c77ceeb8be..036daef64add96280cf60ece88e1ac3bda41b2dd
@@@ -1,11 -1,6 +1,7 @@@
  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.BugException;
  import net.sf.openrocket.util.Coordinate;
  import net.sf.openrocket.util.MathUtil;
  
@@@ -314,10 -305,19 +314,19 @@@ public class InnerTube extends Thicknes
                
                return new Coordinate(this.getLength() - motor.getLength() + this.getMotorOverhang());
        }
 -
        
+     /**
+      * 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);
+     }
        
 -      
 +
 +
        /*
         * (non-Javadoc)
         * Copy the motor and ejection delay HashMaps.
index 6e3f7b39d48331697454855f9ce1adbed5695a3c,2670bc6063741d925b82dbd47926bb3b4df69d4d..500e9063bd34b31675b3ba88197b29e7dd40ae5f
@@@ -3,12 -6,9 +6,10 @@@ import net.sf.openrocket.util.MathUtil
  import java.util.ArrayList;
  import java.util.Collection;
  
- import net.sf.openrocket.util.Coordinate;
- import net.sf.openrocket.util.MathUtil;
  
- public class LaunchLug extends ExternalComponent {
-       
 +
+ public class LaunchLug extends ExternalComponent implements Coaxial {
        private double radius;
        private double thickness;
        
        }
        
        
-       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;
                this.thickness = Math.min(this.thickness, this.radius);
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
 -
 +      
        public double getInnerRadius() {
 -              return radius-thickness;
 +              return radius - thickness;
        }
 -
 +      
        public void setInnerRadius(double innerRadius) {
-               setRadius(innerRadius + thickness);
+               setOuterRadius(innerRadius + thickness);
        }
 -
 +      
        public double getThickness() {
                return thickness;
        }
        @Override
        public double getLongitudalUnitInertia() {
                // 1/12 * (3 * (r1^2 + r2^2) + h^2)
-               return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getRadius()) + MathUtil.pow2(getLength())) / 12;
+               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;
        }
 -
 -
 +      
 +      @Override
 +      public boolean allowsChildren() {
 +              return false;
 +      }
 +      
        @Override
        public boolean isCompatible(Class<? extends RocketComponent> type) {
                // Allow nothing to be attached to a LaunchLug
                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 7ba088b9fad1a452c8b9e89cf9cf400525a7fc9e,a0c923ed2592219aaa14c0b4264cb4bf63db0b05..509c9ff545e98e869b6f1f7601dd771d280a53a7
@@@ -1,26 -1,21 +1,25 @@@
  package net.sf.openrocket.rocketcomponent;
  
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.HashMap;
- import java.util.Iterator;
- 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.gui.main.ExceptionHandler;
 +import net.sf.openrocket.logging.LogHelper;
  import net.sf.openrocket.motor.Motor;
 +import net.sf.openrocket.startup.Application;
  import net.sf.openrocket.util.Chars;
  import net.sf.openrocket.util.Coordinate;
  import net.sf.openrocket.util.MathUtil;
 +import net.sf.openrocket.util.UniqueID;
  
+ import javax.swing.event.ChangeListener;
+ import javax.swing.event.EventListenerList;
+ import java.util.ArrayList;
+ import java.util.Collection;
+ import java.util.Collections;
+ import java.util.HashMap;
+ import java.util.Iterator;
+ import java.util.LinkedList;
+ import java.util.List;
+ import java.util.UUID;
  
  /**
   * Base for all rocket components.  This is the "starting point" for all rocket trees.
index 34ae962f2ba15a4550a86a677544be9a915904dd,71d2c1390821d122ab9c966b30b572313e025e2c..02d4c1d02b2084df927434dfdf21c9a664b9f2a7
@@@ -1,32 -1,27 +1,31 @@@
  package net.sf.openrocket.rocketcomponent;
  
- import java.awt.Color;
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.EmptyStackException;
- import java.util.Iterator;
- import java.util.List;
- import java.util.NoSuchElementException;
- import java.util.Stack;
- import javax.swing.event.ChangeListener;
 +import net.sf.openrocket.logging.LogHelper;
 +import net.sf.openrocket.logging.TraceException;
 +import net.sf.openrocket.startup.Application;
  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 net.sf.openrocket.util.UniqueID;
  
 -import java.util.UUID;
+ import javax.swing.event.ChangeListener;
+ import java.awt.Color;
+ import java.util.ArrayList;
+ import java.util.Collection;
+ import java.util.Collections;
+ import java.util.EmptyStackException;
+ import java.util.Iterator;
+ import java.util.List;
+ import java.util.NoSuchElementException;
+ import java.util.Stack;
  
 -public abstract class RocketComponent implements ChangeSource, Cloneable, 
 +public abstract class RocketComponent implements ChangeSource, Cloneable,
-               Iterable<RocketComponent> {
+               Iterable<RocketComponent> , Visitable<ComponentVisitor, RocketComponent> {
 -
 +      private static final LogHelper log = Application.getLogger();
 +      
        /*
         * Text is suitable to the form
         *    Position relative to:  <title>
                // These must not fire any events, due to Rocket undo system initialization
                this.name = getComponentName();
                this.relativePosition = relativePosition;
 -              this.id = UUID.randomUUID().toString();
 +              newID();
        }
-       
-       
  
+     ////////////  Methods that must be implemented  ////////////
  
  
-       ////////////  Methods that must be implemented  ////////////
-       
        /**
         * Static component name.  The name may not vary of the parameters, it must be static.
         */
                        clone.children.add(childCopy);
                        childCopy.parent = clone;
                }
 -
 +              
                return clone;
        }
 -
 -
 +      
 +      
+     /**
+      * 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  ////////////
        
 -      
 +
  
        ////////// Common parameter setting/getting //////////
        
index 361f50232fcd1cd4c3b2d9aec694018e9c348763,ed2fc7a9129d5f20b69ffcf8b90fc12b7148a5e8..b16cd4b660e96a39c42d99533f66061cea79080f
@@@ -1,28 -1,32 +1,38 @@@
  package net.sf.openrocket.rocketcomponent;
  
  public class Stage extends ComponentAssembly {
 -
 +      
-       @Override
-       public String getComponentName() {
-               return "Stage";
-       }
+     @Override
+     public String getComponentName () {
+         return "Stage";
+     }
 -
 -
 +      
 +      
 +      @Override
 +      public boolean allowsChildren() {
 +              return true;
 +      }
 +      
-       /**
+     /**
 -     * Check whether the given type can be added to this component.  A Stage allows only BodyComponents to be added.
 +       * 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);
-       }
-       
+      *
+      * @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 53b1a196e6246843a8407e4ffde501780047e55c,f26666ae9079415722dedf1420f9419bd2b2fde6..81140a66dc60b208c821d433d2adcaaf279b233c
@@@ -354,13 -357,13 +357,13 @@@ public class Transition extends Symmetr
        /**
         * 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;
 +              double min = 0, max = length;
                
                if (r1 >= r2) {
 -                      double tmp=r1;
 +                      double tmp = r1;
                        r1 = r2;
                        r2 = tmp;
                }
                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);
+     }
        
 +      /**
 +       * Check whether the given type can be added to this component.  Transitions allow any
 +       * InternalComponents 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) {
 +              if (InternalComponent.class.isAssignableFrom(type))
 +                      return true;
 +              return false;
 +      }
 +      
        
 +
        /**
         * An enumeration listing the possible shapes of transitions.
         *