4 package net.sf.openrocket.gui.print;
6 import java.awt.Graphics2D;
7 import java.io.IOException;
8 import java.text.DecimalFormat;
9 import java.util.ArrayList;
10 import java.util.Iterator;
11 import java.util.List;
13 import net.sf.openrocket.document.OpenRocketDocument;
14 import net.sf.openrocket.document.Simulation;
15 import net.sf.openrocket.gui.figureelements.FigureElement;
16 import net.sf.openrocket.gui.figureelements.RocketInfo;
17 import net.sf.openrocket.gui.scalefigure.RocketPanel;
18 import net.sf.openrocket.logging.LogHelper;
19 import net.sf.openrocket.masscalc.BasicMassCalculator;
20 import net.sf.openrocket.masscalc.MassCalculator;
21 import net.sf.openrocket.masscalc.MassCalculator.MassCalcType;
22 import net.sf.openrocket.motor.Motor;
23 import net.sf.openrocket.rocketcomponent.Configuration;
24 import net.sf.openrocket.rocketcomponent.MotorMount;
25 import net.sf.openrocket.rocketcomponent.Rocket;
26 import net.sf.openrocket.rocketcomponent.RocketComponent;
27 import net.sf.openrocket.rocketcomponent.Stage;
28 import net.sf.openrocket.simulation.FlightData;
29 import net.sf.openrocket.simulation.exception.SimulationException;
30 import net.sf.openrocket.startup.Application;
31 import net.sf.openrocket.unit.Unit;
32 import net.sf.openrocket.unit.UnitGroup;
33 import net.sf.openrocket.util.Chars;
34 import net.sf.openrocket.util.Coordinate;
35 import net.sf.openrocket.util.Prefs;
37 import com.itextpdf.text.Document;
38 import com.itextpdf.text.DocumentException;
39 import com.itextpdf.text.Element;
40 import com.itextpdf.text.Paragraph;
41 import com.itextpdf.text.Rectangle;
42 import com.itextpdf.text.pdf.BaseFont;
43 import com.itextpdf.text.pdf.DefaultFontMapper;
44 import com.itextpdf.text.pdf.PdfContentByte;
45 import com.itextpdf.text.pdf.PdfPCell;
46 import com.itextpdf.text.pdf.PdfPTable;
47 import com.itextpdf.text.pdf.PdfWriter;
51 * # Title # Section describing the rocket in general without motors
52 * # Section describing the rocket in general without motors
57 * CP position at 5 degree AOA (or similar)
59 * parachute/streamer sizes
60 * max. diameter (caliber)
61 * velocity at exit of rail/rod
62 * minimum safe velocity reached in x inches/cm
64 * # Section for each motor configuration
66 * a summary of the motors, e.g. 3xC6-0; B4-6
67 * a list of the motors including the manufacturer, designation (maybe also info like burn time, grams of propellant,
69 * total grams of propellant
72 * CG and CP position, stability margin
73 * predicted flight altitude, max. velocity and max. acceleration
74 * predicted velocity at chute deployment
75 * predicted descent rate
76 * Thrust to Weight Ratio of each stage
80 public class DesignReport {
85 private static final LogHelper log = Application.getLogger();
90 private OpenRocketDocument rocketDocument;
93 * A panel used for rendering of the design diagram.
95 final RocketPanel panel;
100 protected Document document;
102 /** The displayed strings. */
103 private static final String STAGES = "Stages: ";
104 private static final String MASS_WITH_MOTORS = "Mass (with motors): ";
105 private static final String MASS_WITH_MOTOR = "Mass (with motor): ";
106 private static final String MASS_EMPTY = "Mass (Empty): ";
107 private static final String STABILITY = "Stability: ";
108 private static final String CG = "CG: ";
109 private static final String CP = "CP: ";
110 private static final String MOTOR = "Motor";
111 private static final String AVG_THRUST = "Avg Thrust";
112 private static final String BURN_TIME = "Burn Time";
113 private static final String MAX_THRUST = "Max Thrust";
114 private static final String TOTAL_IMPULSE = "Total Impulse";
115 private static final String THRUST_TO_WT = "Thrust to Wt";
116 private static final String PROPELLANT_WT = "Propellant Wt";
117 private static final String SIZE = "Size";
118 private static final String ALTITUDE = "Altitude";
119 private static final String FLIGHT_TIME = "Flight Time";
120 private static final String TIME_TO_APOGEE = "Time to Apogee";
121 private static final String VELOCITY_OFF_PAD = "Velocity off Pad";
122 private static final String MAX_VELOCITY = "Max Velocity";
123 private static final String LANDING_VELOCITY = "Landing Velocity";
124 private static final String ROCKET_DESIGN = "Rocket Design";
125 private static final double GRAVITY_CONSTANT = 9.80665d;
130 * @param theRocDoc the OR document
131 * @param theIDoc the iText document
133 public DesignReport(OpenRocketDocument theRocDoc, Document theIDoc) {
135 rocketDocument = theRocDoc;
136 panel = new RocketPanel(rocketDocument);
140 * Main entry point. Prints the rocket drawing and design data.
142 * @param writer a direct byte writer
144 public void writeToDocument(PdfWriter writer) {
145 if (writer == null) {
148 com.itextpdf.text.Rectangle pageSize = document.getPageSize();
149 int pageImageableWidth = (int) pageSize.getWidth() - (int) pageSize.getBorderWidth() * 2;
150 int pageImageableHeight = (int) pageSize.getHeight() / 2 - (int) pageSize.getBorderWidthTop();
152 PrintUtilities.addText(document, PrintUtilities.BIG_BOLD, ROCKET_DESIGN);
154 Rocket rocket = rocketDocument.getRocket();
155 final Configuration configuration = rocket.getDefaultConfiguration();
156 configuration.setAllStages();
157 PdfContentByte canvas = writer.getDirectContent();
159 final PrintFigure figure = new PrintFigure(configuration);
161 FigureElement cp = panel.getExtraCP();
162 FigureElement cg = panel.getExtraCG();
163 RocketInfo text = panel.getExtraText();
165 double scale = paintRocketDiagram(pageImageableWidth, pageImageableHeight, canvas, figure, cp, cg);
169 canvas.setFontAndSize(BaseFont.createFont(PrintUtilities.NORMAL.getFamilyname(), BaseFont.CP1252,
170 BaseFont.EMBEDDED), PrintUtilities.NORMAL_FONT_SIZE);
171 } catch (DocumentException e) {
172 log.error("Could not set font.", e);
173 } catch (IOException e) {
174 log.error("Could not create font.", e);
176 int figHeightPts = (int) (PrintUnit.METERS.toPoints(figure.getFigureHeight()) * 0.4 * (scale / PrintUnit.METERS
178 final int diagramHeight = pageImageableHeight * 2 - 70 - (figHeightPts);
179 canvas.moveText(document.leftMargin() + pageSize.getBorderWidthLeft(), diagramHeight);
180 canvas.moveTextWithLeading(0, -16);
182 float initialY = canvas.getYTLM();
184 canvas.showText(rocketDocument.getRocket().getName());
186 canvas.newlineShowText(STAGES);
187 canvas.showText("" + rocket.getStageCount());
190 if (configuration.hasMotors()) {
191 if (configuration.getStageCount() > 1) {
192 canvas.newlineShowText(MASS_WITH_MOTORS);
194 canvas.newlineShowText(MASS_WITH_MOTOR);
197 canvas.newlineShowText(MASS_EMPTY);
199 canvas.showText(text.getMass(UnitGroup.UNITS_MASS.getDefaultUnit()));
201 canvas.newlineShowText(STABILITY);
202 canvas.showText(text.getStability());
204 canvas.newlineShowText(CG);
205 canvas.showText(text.getCg());
207 canvas.newlineShowText(CP);
208 canvas.showText(text.getCp());
212 //Move the internal pointer of the document below that of what was just written using the direct byte buffer.
213 Paragraph paragraph = new Paragraph();
214 float finalY = canvas.getYTLM();
215 int heightOfDiagramAndText = (int) (pageSize.getHeight() - (finalY - initialY + diagramHeight));
217 paragraph.setSpacingAfter(heightOfDiagramAndText);
218 document.add(paragraph);
220 String[] motorIds = rocket.getMotorConfigurationIDs();
222 List<Double> stageMasses = getStageMasses(rocket);
224 for (int j = 0; j < motorIds.length; j++) {
225 String motorId = motorIds[j];
226 if (motorId != null) {
227 PdfPTable parent = new PdfPTable(2);
228 parent.setWidthPercentage(100);
229 parent.setHorizontalAlignment(Element.ALIGN_LEFT);
230 parent.setSpacingBefore(0);
231 parent.setWidths(new int[] { 1, 3 });
233 //The first motor config is always null. Skip it and the top-most motor, then set the leading.
237 addFlightData(rocket, motorId, parent, leading);
238 addMotorData(rocket, motorId, parent);
239 document.add(parent);
242 } catch (DocumentException e) {
243 log.error("Could not modify document.", e);
248 * Get the motor list for all motor mounts.
250 * @param theRocket the rocket object
251 * @param theMid the motor id
253 * @return a list of Motor
255 private List<Motor> getMotorList(final Rocket theRocket, final String theMid) {
256 Iterator<RocketComponent> components = theRocket.iterator();
257 final List<Motor> motorList = new ArrayList<Motor>();
258 while (components.hasNext()) {
259 RocketComponent rocketComponent = components.next();
260 if (rocketComponent instanceof MotorMount) {
261 MotorMount mm = (MotorMount) rocketComponent;
262 final Motor motor = mm.getMotor(theMid);
264 motorList.add(motor);
272 * Paint a diagram of the rocket into the PDF document.
274 * @param thePageImageableWidth the number of points in the width of the page available for drawing
275 * @param thePageImageableHeight the number of points in the height of the page available for drawing
276 * @param theCanvas the direct byte writer
277 * @param theFigure the print figure
278 * @param theCp the center of pressure figure element
279 * @param theCg the center of gravity figure element
281 * @return the scale of the diagram
283 private double paintRocketDiagram(final int thePageImageableWidth, final int thePageImageableHeight,
284 final PdfContentByte theCanvas, final PrintFigure theFigure,
285 final FigureElement theCp, final FigureElement theCg) {
286 theFigure.clearAbsoluteExtra();
287 theFigure.clearRelativeExtra();
288 theFigure.addRelativeExtra(theCp);
289 theFigure.addRelativeExtra(theCg);
290 theFigure.updateFigure();
293 (thePageImageableWidth * 2.2) / theFigure.getFigureWidth();
295 theFigure.setScale(scale);
297 * page dimensions are in points-per-inch, which, in Java2D, are the same as pixels-per-inch; thus we don't need any conversion
299 theFigure.setSize(thePageImageableWidth, thePageImageableHeight);
300 theFigure.updateFigure();
303 final DefaultFontMapper mapper = new DefaultFontMapper();
304 Graphics2D g2d = theCanvas.createGraphics(thePageImageableWidth, thePageImageableHeight * 2, mapper);
305 g2d.translate(20, 120);
307 g2d.scale(0.4d, 0.4d);
308 theFigure.paint(g2d);
314 * Add the motor data for a motor configuration to the table.
316 * @param rocket the rocket
317 * @param motorId the motor ID to output
318 * @param parent the parent to which the motor data will be added
320 private void addMotorData(Rocket rocket, String motorId, final PdfPTable parent) {
322 PdfPTable motorTable = new PdfPTable(8);
323 motorTable.setWidthPercentage(68);
324 motorTable.setHorizontalAlignment(Element.ALIGN_LEFT);
326 final PdfPCell motorCell = ITextHelper.createCell(MOTOR, PdfPCell.BOTTOM);
328 motorCell.setPaddingLeft(mPad);
329 motorTable.addCell(motorCell);
330 motorTable.addCell(ITextHelper.createCell(AVG_THRUST, PdfPCell.BOTTOM));
331 motorTable.addCell(ITextHelper.createCell(BURN_TIME, PdfPCell.BOTTOM));
332 motorTable.addCell(ITextHelper.createCell(MAX_THRUST, PdfPCell.BOTTOM));
333 motorTable.addCell(ITextHelper.createCell(TOTAL_IMPULSE, PdfPCell.BOTTOM));
334 motorTable.addCell(ITextHelper.createCell(THRUST_TO_WT, PdfPCell.BOTTOM));
335 motorTable.addCell(ITextHelper.createCell(PROPELLANT_WT, PdfPCell.BOTTOM));
336 motorTable.addCell(ITextHelper.createCell(SIZE, PdfPCell.BOTTOM));
338 DecimalFormat ttwFormat = new DecimalFormat("0.00");
340 MassCalculator massCalc = new BasicMassCalculator();
342 Configuration config = new Configuration(rocket);
343 config.setMotorConfigurationID(motorId);
345 int totalMotorCount = 0;
346 double totalPropMass = 0;
347 double totalImpulse = 0;
351 double stageMass = 0;
353 boolean topBorder = false;
354 for (RocketComponent c : rocket) {
356 if (c instanceof Stage) {
357 config.setToStage(stage);
359 stageMass = massCalc.getCG(config, MassCalcType.LAUNCH_MASS).weight;
360 // Calculate total thrust-to-weight from only lowest stage motors
365 if (c instanceof MotorMount && ((MotorMount) c).isMotorMount()) {
366 MotorMount mount = (MotorMount) c;
368 if (mount.isMotorMount() && mount.getMotor(motorId) != null) {
369 Motor motor = mount.getMotor(motorId);
370 int motorCount = c.toAbsolute(Coordinate.NUL).length;
373 int border = Rectangle.NO_BORDER;
375 border = Rectangle.TOP;
379 String name = motor.getDesignation();
380 if (motorCount > 1) {
381 name += " (" + Chars.TIMES + motorCount + ")";
384 final PdfPCell motorVCell = ITextHelper.createCell(name, border);
385 motorVCell.setPaddingLeft(mPad);
386 motorTable.addCell(motorVCell);
387 motorTable.addCell(ITextHelper.createCell(
388 UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(motor.getAverageThrustEstimate()), border));
389 motorTable.addCell(ITextHelper.createCell(
390 UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit().toStringUnit(motor.getBurnTimeEstimate()), border));
391 motorTable.addCell(ITextHelper.createCell(
392 UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(motor.getMaxThrustEstimate()), border));
393 motorTable.addCell(ITextHelper.createCell(
394 UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit(motor.getTotalImpulseEstimate()), border));
396 double ttw = motor.getAverageThrustEstimate() / (stageMass * GRAVITY_CONSTANT);
397 motorTable.addCell(ITextHelper.createCell(
398 ttwFormat.format(ttw) + ":1", border));
400 double propMass = (motor.getLaunchCG().weight - motor.getEmptyCG().weight);
401 motorTable.addCell(ITextHelper.createCell(
402 UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(propMass), border));
404 final Unit motorUnit = UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit();
405 motorTable.addCell(ITextHelper.createCell(motorUnit.toString(motor.getDiameter()) +
407 motorUnit.toString(motor.getLength()) + " " +
408 motorUnit.toString(), border));
410 // Sum up total count
411 totalMotorCount += motorCount;
412 totalPropMass += propMass * motorCount;
413 totalImpulse += motor.getTotalImpulseEstimate() * motorCount;
414 totalTTW += ttw * motorCount;
419 if (totalMotorCount > 1) {
420 int border = Rectangle.TOP;
421 final PdfPCell motorVCell = ITextHelper.createCell("Total:", border);
422 motorVCell.setPaddingLeft(mPad);
423 motorTable.addCell(motorVCell);
424 motorTable.addCell(ITextHelper.createCell("", border));
425 motorTable.addCell(ITextHelper.createCell("", border));
426 motorTable.addCell(ITextHelper.createCell("", border));
427 motorTable.addCell(ITextHelper.createCell(
428 UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit(totalImpulse), border));
429 motorTable.addCell(ITextHelper.createCell(
430 ttwFormat.format(totalTTW) + ":1", border));
431 motorTable.addCell(ITextHelper.createCell(
432 UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(totalPropMass), border));
433 motorTable.addCell(ITextHelper.createCell("", border));
437 PdfPCell c = new PdfPCell(motorTable);
438 c.setBorder(PdfPCell.LEFT);
439 c.setBorderWidthTop(0f);
445 * Add the motor data for a motor configuration to the table.
447 * @param theRocket the rocket
448 * @param motorId a motor configuration id
449 * @param parent the parent to which the motor data will be added
450 * @param leading the number of points for the leading
452 private void addFlightData(final Rocket theRocket, final String motorId, final PdfPTable parent, int leading) {
454 // Perform flight simulation
455 Rocket duplicate = theRocket.copyWithOriginalID();
456 FlightData flight = null;
458 Simulation simulation = Prefs.getBackgroundSimulation(duplicate);
459 simulation.getConditions().setMotorConfigurationID(motorId);
460 simulation.simulate();
461 flight = simulation.getSimulatedData();
462 } catch (SimulationException e1) {
466 // Output the flight data
467 if (flight != null) {
469 final Unit distanceUnit = UnitGroup.UNITS_DISTANCE.getDefaultUnit();
470 final Unit velocityUnit = UnitGroup.UNITS_VELOCITY.getDefaultUnit();
471 final Unit flightTimeUnit = UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit();
473 PdfPTable labelTable = new PdfPTable(2);
474 labelTable.setWidths(new int[] { 3, 2 });
475 final Paragraph chunk = ITextHelper.createParagraph(stripBrackets(
476 theRocket.getMotorConfigurationNameOrDescription(motorId)), PrintUtilities.BOLD);
477 chunk.setLeading(leading);
478 chunk.setSpacingAfter(3f);
482 final PdfPCell cell = ITextHelper.createCell(ALTITUDE, 2, 2);
483 cell.setUseBorderPadding(false);
484 cell.setBorderWidthTop(0f);
485 labelTable.addCell(cell);
486 labelTable.addCell(ITextHelper.createCell(distanceUnit.toStringUnit(flight.getMaxAltitude()), 2, 2));
488 labelTable.addCell(ITextHelper.createCell(FLIGHT_TIME, 2, 2));
489 labelTable.addCell(ITextHelper.createCell(flightTimeUnit.toStringUnit(flight.getFlightTime()), 2, 2));
491 labelTable.addCell(ITextHelper.createCell(TIME_TO_APOGEE, 2, 2));
492 labelTable.addCell(ITextHelper.createCell(flightTimeUnit.toStringUnit(flight.getTimeToApogee()), 2, 2));
494 labelTable.addCell(ITextHelper.createCell(VELOCITY_OFF_PAD, 2, 2));
495 labelTable.addCell(ITextHelper.createCell(velocityUnit.toStringUnit(flight.getLaunchRodVelocity()), 2, 2));
497 labelTable.addCell(ITextHelper.createCell(MAX_VELOCITY, 2, 2));
498 labelTable.addCell(ITextHelper.createCell(velocityUnit.toStringUnit(flight.getMaxVelocity()), 2, 2));
500 labelTable.addCell(ITextHelper.createCell(LANDING_VELOCITY, 2, 2));
501 labelTable.addCell(ITextHelper.createCell(velocityUnit.toStringUnit(flight.getGroundHitVelocity()), 2, 2));
503 //Add the table to the parent; have to wrap it in a cell
504 PdfPCell c = new PdfPCell(labelTable);
505 c.setBorder(PdfPCell.RIGHT);
506 c.setBorderWidthTop(0);
509 } catch (DocumentException e) {
510 log.error("Could not add flight data to document.", e);
516 * Strip [] brackets from a string.
518 * @param target the original string
520 * @return target with [] removed
522 private String stripBrackets(String target) {
523 return stripLeftBracket(stripRightBracket(target));
527 * Strip [ from a string.
529 * @param target the original string
531 * @return target with [ removed
533 private String stripLeftBracket(String target) {
534 return target.replace("[", "");
538 * Strip ] from a string.
540 * @param target the original string
542 * @return target with ] removed
544 private String stripRightBracket(String target) {
545 return target.replace("]", "");
550 * Return a list of cumulative stage masses. The latter masses include the mass
551 * of the upper stages as well.
553 * @param rocket the rocket
554 * @return a list containing the cumulative stage masses
556 private List<Double> getStageMasses(final Rocket rocket) {
557 List<Double> masses = new ArrayList<Double>();
558 int stages = rocket.getStageCount();
560 Configuration config = new Configuration(rocket);
561 MassCalculator calc = new BasicMassCalculator();
563 for (int i = 0; i < stages; i++) {
564 config.setToStage(i);
565 masses.add(calc.getCG(config, MassCalcType.NO_MOTORS).weight);