4 package net.sf.openrocket.gui.print;
6 import java.awt.Graphics2D;
7 import java.io.IOException;
8 import java.text.DecimalFormat;
11 import net.sf.openrocket.document.OpenRocketDocument;
12 import net.sf.openrocket.document.Simulation;
13 import net.sf.openrocket.gui.figureelements.FigureElement;
14 import net.sf.openrocket.gui.figureelements.RocketInfo;
15 import net.sf.openrocket.gui.scalefigure.RocketPanel;
16 import net.sf.openrocket.logging.LogHelper;
17 import net.sf.openrocket.masscalc.BasicMassCalculator;
18 import net.sf.openrocket.masscalc.MassCalculator;
19 import net.sf.openrocket.masscalc.MassCalculator.MassCalcType;
20 import net.sf.openrocket.motor.Motor;
21 import net.sf.openrocket.rocketcomponent.Configuration;
22 import net.sf.openrocket.rocketcomponent.MotorMount;
23 import net.sf.openrocket.rocketcomponent.Rocket;
24 import net.sf.openrocket.rocketcomponent.RocketComponent;
25 import net.sf.openrocket.rocketcomponent.Stage;
26 import net.sf.openrocket.simulation.FlightData;
27 import net.sf.openrocket.simulation.exception.SimulationException;
28 import net.sf.openrocket.startup.Application;
29 import net.sf.openrocket.unit.Unit;
30 import net.sf.openrocket.unit.UnitGroup;
31 import net.sf.openrocket.util.Chars;
32 import net.sf.openrocket.util.Coordinate;
34 import com.itextpdf.text.Document;
35 import com.itextpdf.text.DocumentException;
36 import com.itextpdf.text.Element;
37 import com.itextpdf.text.Paragraph;
38 import com.itextpdf.text.Rectangle;
39 import com.itextpdf.text.pdf.BaseFont;
40 import com.itextpdf.text.pdf.DefaultFontMapper;
41 import com.itextpdf.text.pdf.PdfContentByte;
42 import com.itextpdf.text.pdf.PdfPCell;
43 import com.itextpdf.text.pdf.PdfPTable;
44 import com.itextpdf.text.pdf.PdfWriter;
48 * # Title # Section describing the rocket in general without motors
49 * # Section describing the rocket in general without motors
54 * CP position at 5 degree AOA (or similar)
56 * parachute/streamer sizes
57 * max. diameter (caliber)
58 * velocity at exit of rail/rod
59 * minimum safe velocity reached in x inches/cm
61 * # Section for each motor configuration
63 * a summary of the motors, e.g. 3xC6-0; B4-6
64 * a list of the motors including the manufacturer, designation (maybe also info like burn time, grams of propellant,
66 * total grams of propellant
69 * CG and CP position, stability margin
70 * predicted flight altitude, max. velocity and max. acceleration
71 * predicted velocity at chute deployment
72 * predicted descent rate
73 * Thrust to Weight Ratio of each stage
77 public class DesignReport {
82 private static final LogHelper log = Application.getLogger();
83 public static final double SCALE_FUDGE_FACTOR = 0.4d;
88 private OpenRocketDocument rocketDocument;
91 * A panel used for rendering of the design diagram.
93 final RocketPanel panel;
98 protected Document document;
101 * The figure rotation.
103 private double rotation = 0d;
105 /** The displayed strings. */
106 private static final String STAGES = "Stages: ";
107 private static final String MASS_WITH_MOTORS = "Mass (with motors): ";
108 private static final String MASS_WITH_MOTOR = "Mass (with motor): ";
109 private static final String MASS_EMPTY = "Mass (Empty): ";
110 private static final String STABILITY = "Stability: ";
111 private static final String CG = "CG: ";
112 private static final String CP = "CP: ";
113 private static final String MOTOR = "Motor";
114 private static final String AVG_THRUST = "Avg Thrust";
115 private static final String BURN_TIME = "Burn Time";
116 private static final String MAX_THRUST = "Max Thrust";
117 private static final String TOTAL_IMPULSE = "Total Impulse";
118 private static final String THRUST_TO_WT = "Thrust to Wt";
119 private static final String PROPELLANT_WT = "Propellant Wt";
120 private static final String SIZE = "Size";
121 private static final String ALTITUDE = "Altitude";
122 private static final String FLIGHT_TIME = "Flight Time";
123 private static final String TIME_TO_APOGEE = "Time to Apogee";
124 private static final String VELOCITY_OFF_PAD = "Velocity off Pad";
125 private static final String MAX_VELOCITY = "Max Velocity";
126 private static final String DEPLOYMENT_VELOCITY = "Velocity at Deployment";
127 private static final String LANDING_VELOCITY = "Landing Velocity";
128 private static final String ROCKET_DESIGN = "Rocket Design";
129 private static final double GRAVITY_CONSTANT = 9.80665d;
134 * @param theRocDoc the OR document
135 * @param theIDoc the iText document
136 * @param figureRotation the angle the figure is rotated on the screen; printed report will mimic
138 public DesignReport(OpenRocketDocument theRocDoc, Document theIDoc, Double figureRotation) {
140 rocketDocument = theRocDoc;
141 panel = new RocketPanel(rocketDocument);
142 rotation = figureRotation;
146 * Main entry point. Prints the rocket drawing and design data.
148 * @param writer a direct byte writer
150 public void writeToDocument(PdfWriter writer) {
151 if (writer == null) {
154 com.itextpdf.text.Rectangle pageSize = document.getPageSize();
155 int pageImageableWidth = (int) pageSize.getWidth() - (int) pageSize.getBorderWidth() * 2;
156 int pageImageableHeight = (int) pageSize.getHeight() / 2 - (int) pageSize.getBorderWidthTop();
158 PrintUtilities.addText(document, PrintUtilities.BIG_BOLD, ROCKET_DESIGN);
160 Rocket rocket = rocketDocument.getRocket();
161 final Configuration configuration = rocket.getDefaultConfiguration().clone();
162 configuration.setAllStages();
163 PdfContentByte canvas = writer.getDirectContent();
165 final PrintFigure figure = new PrintFigure(configuration);
166 figure.setRotation(rotation);
168 FigureElement cp = panel.getExtraCP();
169 FigureElement cg = panel.getExtraCG();
170 RocketInfo text = panel.getExtraText();
172 double scale = paintRocketDiagram(pageImageableWidth, pageImageableHeight, canvas, figure, cp, cg);
176 canvas.setFontAndSize(BaseFont.createFont(PrintUtilities.NORMAL.getFamilyname(), BaseFont.CP1252,
177 BaseFont.EMBEDDED), PrintUtilities.NORMAL_FONT_SIZE);
178 } catch (DocumentException e) {
179 log.error("Could not set font.", e);
180 } catch (IOException e) {
181 log.error("Could not create font.", e);
183 int figHeightPts = (int) (PrintUnit.METERS.toPoints(figure.getFigureHeight()) * 0.4 * (scale / PrintUnit.METERS
185 final int diagramHeight = pageImageableHeight * 2 - 70 - (figHeightPts);
186 canvas.moveText(document.leftMargin() + pageSize.getBorderWidthLeft(), diagramHeight);
187 canvas.moveTextWithLeading(0, -16);
189 float initialY = canvas.getYTLM();
191 canvas.showText(rocketDocument.getRocket().getName());
193 canvas.newlineShowText(STAGES);
194 canvas.showText("" + rocket.getStageCount());
197 if (configuration.hasMotors()) {
198 if (configuration.getStageCount() > 1) {
199 canvas.newlineShowText(MASS_WITH_MOTORS);
201 canvas.newlineShowText(MASS_WITH_MOTOR);
204 canvas.newlineShowText(MASS_EMPTY);
206 canvas.showText(text.getMass(UnitGroup.UNITS_MASS.getDefaultUnit()));
208 canvas.newlineShowText(STABILITY);
209 canvas.showText(text.getStability());
211 canvas.newlineShowText(CG);
212 canvas.showText(text.getCg());
214 canvas.newlineShowText(CP);
215 canvas.showText(text.getCp());
219 //Move the internal pointer of the document below that of what was just written using the direct byte buffer.
220 Paragraph paragraph = new Paragraph();
221 float finalY = canvas.getYTLM();
222 int heightOfDiagramAndText = (int) (pageSize.getHeight() - (finalY - initialY + diagramHeight));
224 paragraph.setSpacingAfter(heightOfDiagramAndText);
225 document.add(paragraph);
227 String[] motorIds = rocket.getMotorConfigurationIDs();
228 List<Simulation> simulations = rocketDocument.getSimulations();
230 for (int j = 0; j < motorIds.length; j++) {
231 String motorId = motorIds[j];
232 if (motorId != null) {
233 PdfPTable parent = new PdfPTable(2);
234 parent.setWidthPercentage(100);
235 parent.setHorizontalAlignment(Element.ALIGN_LEFT);
236 parent.setSpacingBefore(0);
237 parent.setWidths(new int[] { 1, 3 });
239 //The first motor config is always null. Skip it and the top-most motor, then set the leading.
243 FlightData flight = findSimulation(motorId, simulations);
244 addFlightData(flight, rocket, motorId, parent, leading);
245 addMotorData(rocket, motorId, parent);
246 document.add(parent);
249 } catch (DocumentException e) {
250 log.error("Could not modify document.", e);
256 * Paint a diagram of the rocket into the PDF document.
258 * @param thePageImageableWidth the number of points in the width of the page available for drawing
259 * @param thePageImageableHeight the number of points in the height of the page available for drawing
260 * @param theCanvas the direct byte writer
261 * @param theFigure the print figure
262 * @param theCp the center of pressure figure element
263 * @param theCg the center of gravity figure element
265 * @return the scale of the diagram
267 private double paintRocketDiagram(final int thePageImageableWidth, final int thePageImageableHeight,
268 final PdfContentByte theCanvas, final PrintFigure theFigure,
269 final FigureElement theCp, final FigureElement theCg) {
270 theFigure.clearAbsoluteExtra();
271 theFigure.clearRelativeExtra();
272 theFigure.addRelativeExtra(theCp);
273 theFigure.addRelativeExtra(theCg);
274 theFigure.updateFigure();
277 (thePageImageableWidth * 2.2) / theFigure.getFigureWidth();
278 theFigure.setScale(scale);
280 * page dimensions are in points-per-inch, which, in Java2D, are the same as pixels-per-inch; thus we don't need any conversion
282 theFigure.setSize(thePageImageableWidth, thePageImageableHeight);
283 theFigure.updateFigure();
285 final DefaultFontMapper mapper = new DefaultFontMapper();
286 Graphics2D g2d = theCanvas.createGraphics(thePageImageableWidth, thePageImageableHeight * 2, mapper);
287 final double halfFigureHeight = SCALE_FUDGE_FACTOR * theFigure.getFigureHeightPx()/2;
288 int y = PrintUnit.POINTS_PER_INCH;
289 //If the y dimension is negative, then it will potentially be drawn off the top of the page. Move the origin
291 if (theFigure.getDimensions().getY() < 0.0d) {
292 y += (int)halfFigureHeight;
294 g2d.translate(20, y);
296 g2d.scale(SCALE_FUDGE_FACTOR, SCALE_FUDGE_FACTOR);
297 theFigure.paint(g2d);
303 * Add the motor data for a motor configuration to the table.
305 * @param rocket the rocket
306 * @param motorId the motor ID to output
307 * @param parent the parent to which the motor data will be added
309 private void addMotorData(Rocket rocket, String motorId, final PdfPTable parent) {
311 PdfPTable motorTable = new PdfPTable(8);
312 motorTable.setWidthPercentage(68);
313 motorTable.setHorizontalAlignment(Element.ALIGN_LEFT);
315 final PdfPCell motorCell = ITextHelper.createCell(MOTOR, PdfPCell.BOTTOM);
317 motorCell.setPaddingLeft(mPad);
318 motorTable.addCell(motorCell);
319 motorTable.addCell(ITextHelper.createCell(AVG_THRUST, PdfPCell.BOTTOM));
320 motorTable.addCell(ITextHelper.createCell(BURN_TIME, PdfPCell.BOTTOM));
321 motorTable.addCell(ITextHelper.createCell(MAX_THRUST, PdfPCell.BOTTOM));
322 motorTable.addCell(ITextHelper.createCell(TOTAL_IMPULSE, PdfPCell.BOTTOM));
323 motorTable.addCell(ITextHelper.createCell(THRUST_TO_WT, PdfPCell.BOTTOM));
324 motorTable.addCell(ITextHelper.createCell(PROPELLANT_WT, PdfPCell.BOTTOM));
325 motorTable.addCell(ITextHelper.createCell(SIZE, PdfPCell.BOTTOM));
327 DecimalFormat ttwFormat = new DecimalFormat("0.00");
329 MassCalculator massCalc = new BasicMassCalculator();
331 Configuration config = new Configuration(rocket);
332 config.setMotorConfigurationID(motorId);
334 int totalMotorCount = 0;
335 double totalPropMass = 0;
336 double totalImpulse = 0;
340 double stageMass = 0;
342 boolean topBorder = false;
343 for (RocketComponent c : rocket) {
345 if (c instanceof Stage) {
346 config.setToStage(stage);
348 stageMass = massCalc.getCG(config, MassCalcType.LAUNCH_MASS).weight;
349 // Calculate total thrust-to-weight from only lowest stage motors
354 if (c instanceof MotorMount && ((MotorMount) c).isMotorMount()) {
355 MotorMount mount = (MotorMount) c;
357 if (mount.isMotorMount() && mount.getMotor(motorId) != null) {
358 Motor motor = mount.getMotor(motorId);
359 int motorCount = c.toAbsolute(Coordinate.NUL).length;
362 int border = Rectangle.NO_BORDER;
364 border = Rectangle.TOP;
368 String name = motor.getDesignation();
369 if (motorCount > 1) {
370 name += " (" + Chars.TIMES + motorCount + ")";
373 final PdfPCell motorVCell = ITextHelper.createCell(name, border);
374 motorVCell.setPaddingLeft(mPad);
375 motorTable.addCell(motorVCell);
376 motorTable.addCell(ITextHelper.createCell(
377 UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(motor.getAverageThrustEstimate()), border));
378 motorTable.addCell(ITextHelper.createCell(
379 UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit().toStringUnit(motor.getBurnTimeEstimate()), border));
380 motorTable.addCell(ITextHelper.createCell(
381 UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(motor.getMaxThrustEstimate()), border));
382 motorTable.addCell(ITextHelper.createCell(
383 UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit(motor.getTotalImpulseEstimate()), border));
385 double ttw = motor.getAverageThrustEstimate() / (stageMass * GRAVITY_CONSTANT);
386 motorTable.addCell(ITextHelper.createCell(
387 ttwFormat.format(ttw) + ":1", border));
389 double propMass = (motor.getLaunchCG().weight - motor.getEmptyCG().weight);
390 motorTable.addCell(ITextHelper.createCell(
391 UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(propMass), border));
393 final Unit motorUnit = UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit();
394 motorTable.addCell(ITextHelper.createCell(motorUnit.toString(motor.getDiameter()) +
396 motorUnit.toString(motor.getLength()) + " " +
397 motorUnit.toString(), border));
399 // Sum up total count
400 totalMotorCount += motorCount;
401 totalPropMass += propMass * motorCount;
402 totalImpulse += motor.getTotalImpulseEstimate() * motorCount;
403 totalTTW += ttw * motorCount;
408 if (totalMotorCount > 1) {
409 int border = Rectangle.TOP;
410 final PdfPCell motorVCell = ITextHelper.createCell("Total:", border);
411 motorVCell.setPaddingLeft(mPad);
412 motorTable.addCell(motorVCell);
413 motorTable.addCell(ITextHelper.createCell("", border));
414 motorTable.addCell(ITextHelper.createCell("", border));
415 motorTable.addCell(ITextHelper.createCell("", border));
416 motorTable.addCell(ITextHelper.createCell(
417 UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit(totalImpulse), border));
418 motorTable.addCell(ITextHelper.createCell(
419 ttwFormat.format(totalTTW) + ":1", border));
420 motorTable.addCell(ITextHelper.createCell(
421 UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(totalPropMass), border));
422 motorTable.addCell(ITextHelper.createCell("", border));
426 PdfPCell c = new PdfPCell(motorTable);
427 c.setBorder(PdfPCell.LEFT);
428 c.setBorderWidthTop(0f);
434 * Add the flight data for a simulation configuration to the table.
436 * @param flight the flight data for a single simulation
437 * @param theRocket the rocket
438 * @param motorId a motor configuration id
439 * @param parent the parent to which the simulation flight data will be added
440 * @param leading the number of points for the leading
442 private void addFlightData(final FlightData flight, final Rocket theRocket, final String motorId, final PdfPTable parent, int leading) {
444 // Output the flight data
445 if (flight != null) {
447 final Unit distanceUnit = UnitGroup.UNITS_DISTANCE.getDefaultUnit();
448 final Unit velocityUnit = UnitGroup.UNITS_VELOCITY.getDefaultUnit();
449 final Unit flightTimeUnit = UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit();
451 PdfPTable labelTable = new PdfPTable(2);
452 labelTable.setWidths(new int[] { 3, 2 });
453 final Paragraph chunk = ITextHelper.createParagraph(stripBrackets(
454 theRocket.getMotorConfigurationNameOrDescription(motorId)), PrintUtilities.BOLD);
455 chunk.setLeading(leading);
456 chunk.setSpacingAfter(3f);
460 final PdfPCell cell = ITextHelper.createCell(ALTITUDE, 2, 2);
461 cell.setUseBorderPadding(false);
462 cell.setBorderWidthTop(0f);
463 labelTable.addCell(cell);
464 labelTable.addCell(ITextHelper.createCell(distanceUnit.toStringUnit(flight.getMaxAltitude()), 2, 2));
466 labelTable.addCell(ITextHelper.createCell(FLIGHT_TIME, 2, 2));
467 labelTable.addCell(ITextHelper.createCell(flightTimeUnit.toStringUnit(flight.getFlightTime()), 2, 2));
469 labelTable.addCell(ITextHelper.createCell(TIME_TO_APOGEE, 2, 2));
470 labelTable.addCell(ITextHelper.createCell(flightTimeUnit.toStringUnit(flight.getTimeToApogee()), 2, 2));
472 labelTable.addCell(ITextHelper.createCell(VELOCITY_OFF_PAD, 2, 2));
473 labelTable.addCell(ITextHelper.createCell(velocityUnit.toStringUnit(flight.getLaunchRodVelocity()), 2, 2));
475 labelTable.addCell(ITextHelper.createCell(MAX_VELOCITY, 2, 2));
476 labelTable.addCell(ITextHelper.createCell(velocityUnit.toStringUnit(flight.getMaxVelocity()), 2, 2));
478 labelTable.addCell(ITextHelper.createCell(DEPLOYMENT_VELOCITY, 2,2));
479 labelTable.addCell(ITextHelper.createCell(velocityUnit.toStringUnit(flight.getDeploymentVelocity()),2,2));
481 labelTable.addCell(ITextHelper.createCell(LANDING_VELOCITY, 2, 2));
482 labelTable.addCell(ITextHelper.createCell(velocityUnit.toStringUnit(flight.getGroundHitVelocity()), 2, 2));
484 //Add the table to the parent; have to wrap it in a cell
485 PdfPCell c = new PdfPCell(labelTable);
486 c.setBorder(PdfPCell.RIGHT);
487 c.setBorderWidthTop(0);
490 } catch (DocumentException e) {
491 log.error("Could not add flight data to document.", e);
497 * Locate the simulation based on the motor id. Copy the simulation and execute it, then return the resulting
500 * @param motorId the motor id corresponding to the simulation to find
501 * @param simulations the list of simulations currently associated with the rocket
503 * @return the flight data from the simulation for the specified motor id, or null if not found
505 private FlightData findSimulation(final String motorId, List<Simulation> simulations) {
506 // Perform flight simulation
507 FlightData flight = null;
509 for (int i = 0; i < simulations.size(); i++) {
510 Simulation simulation = simulations.get(i);
511 if (simulation.getOptions().getMotorConfigurationID().equals(motorId)) {
512 simulation = simulation.copy();
513 simulation.simulate();
514 flight = simulation.getSimulatedData();
518 } catch (SimulationException e1) {
525 * Strip [] brackets from a string.
527 * @param target the original string
529 * @return target with [] removed
531 private String stripBrackets(String target) {
532 return stripLeftBracket(stripRightBracket(target));
536 * Strip [ from a string.
538 * @param target the original string
540 * @return target with [ removed
542 private String stripLeftBracket(String target) {
543 return target.replace("[", "");
547 * Strip ] from a string.
549 * @param target the original string
551 * @return target with ] removed
553 private String stripRightBracket(String target) {
554 return target.replace("]", "");