/* * DesignReport.java */ package net.sf.openrocket.gui.print; import java.awt.Graphics2D; import java.text.DecimalFormat; import java.util.List; import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.document.Simulation; import net.sf.openrocket.formatting.RocketDescriptor; import net.sf.openrocket.gui.figureelements.FigureElement; import net.sf.openrocket.gui.figureelements.RocketInfo; import net.sf.openrocket.gui.scalefigure.RocketPanel; import net.sf.openrocket.masscalc.BasicMassCalculator; import net.sf.openrocket.masscalc.MassCalculator; import net.sf.openrocket.masscalc.MassCalculator.MassCalcType; import net.sf.openrocket.motor.Motor; import net.sf.openrocket.rocketcomponent.Configuration; import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.Stage; import net.sf.openrocket.simulation.FlightData; import net.sf.openrocket.simulation.exception.SimulationException; import net.sf.openrocket.startup.Application; import net.sf.openrocket.unit.Unit; import net.sf.openrocket.unit.UnitGroup; import net.sf.openrocket.util.Chars; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.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; /** * <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 { /** * The logger. */ private static final Logger log = LoggerFactory.getLogger(DesignReport.class); public static final double SCALE_FUDGE_FACTOR = 0.4d; private static final RocketDescriptor descriptor = Application.getInjector().getInstance(RocketDescriptor.class); /** * The OR Document. */ private OpenRocketDocument rocketDocument; /** * A panel used for rendering of the design diagram. */ final RocketPanel panel; /** * The iText document. */ protected Document document; /** * The figure rotation. */ private double rotation = 0d; /** The displayed strings. */ private static final String STAGES = "Stages: "; private static final String MASS_WITH_MOTORS = "Mass (with motors): "; private static final String MASS_WITH_MOTOR = "Mass (with motor): "; private static final String MASS_EMPTY = "Mass (Empty): "; private static final String STABILITY = "Stability: "; private static final String CG = "CG: "; private static final String CP = "CP: "; private static final String MOTOR = "Motor"; private static final String AVG_THRUST = "Avg Thrust"; private static final String BURN_TIME = "Burn Time"; private static final String MAX_THRUST = "Max Thrust"; private static final String TOTAL_IMPULSE = "Total Impulse"; private static final String THRUST_TO_WT = "Thrust to Wt"; private static final String PROPELLANT_WT = "Propellant Wt"; private static final String SIZE = "Size"; private static final String ALTITUDE = "Altitude"; private static final String FLIGHT_TIME = "Flight Time"; private static final String OPTIMUM_DELAY = "Optimum Delay"; private static final String TIME_TO_APOGEE = "Time to Apogee"; private static final String VELOCITY_OFF_PAD = "Velocity off Pad"; private static final String MAX_VELOCITY = "Max Velocity"; private static final String DEPLOYMENT_VELOCITY = "Velocity at Deployment"; private static final String LANDING_VELOCITY = "Landing Velocity"; private static final String ROCKET_DESIGN = "Rocket Design"; private static final double GRAVITY_CONSTANT = 9.80665d; /** * Constructor. * * @param theRocDoc the OR document * @param theIDoc the iText document * @param figureRotation the angle the figure is rotated on the screen; printed report will mimic */ public DesignReport(OpenRocketDocument theRocDoc, Document theIDoc, Double figureRotation) { document = theIDoc; rocketDocument = theRocDoc; panel = new RocketPanel(rocketDocument); rotation = figureRotation; } /** * Main entry point. Prints the rocket drawing and design data. * * @param writer a direct byte writer */ public void writeToDocument(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().clone(); configuration.setAllStages(); PdfContentByte canvas = writer.getDirectContent(); final PrintFigure figure = new PrintFigure(configuration); figure.setRotation(rotation); FigureElement cp = panel.getExtraCP(); FigureElement cg = panel.getExtraCG(); RocketInfo text = panel.getExtraText(); double scale = paintRocketDiagram(pageImageableWidth, pageImageableHeight, canvas, figure, cp, cg); canvas.beginText(); canvas.setFontAndSize(ITextHelper.getBaseFont(), PrintUtilities.NORMAL_FONT_SIZE); int figHeightPts = (int) (PrintUnit.METERS.toPoints(figure.getFigureHeight()) * 0.4 * (scale / PrintUnit.METERS .toPoints(1))); final int diagramHeight = pageImageableHeight * 2 - 70 - (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()) { if (configuration.getStageCount() > 1) { canvas.newlineShowText(MASS_WITH_MOTORS); } else { canvas.newlineShowText(MASS_WITH_MOTOR); } } 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[] motorIds = rocket.getFlightConfigurationIDs(); List<Simulation> simulations = rocketDocument.getSimulations(); for (int j = 0; j < motorIds.length; j++) { String motorId = motorIds[j]; if (motorId != null) { 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; } FlightData flight = findSimulation(motorId, simulations); addFlightData(flight, rocket, motorId, parent, leading); addMotorData(rocket, motorId, parent); document.add(parent); } } } catch (DocumentException e) { log.error("Could not modify document.", e); } } /** * 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); final double halfFigureHeight = SCALE_FUDGE_FACTOR * theFigure.getFigureHeightPx() / 2; int y = PrintUnit.POINTS_PER_INCH; //If the y dimension is negative, then it will potentially be drawn off the top of the page. Move the origin //to allow for this. if (theFigure.getDimensions().getY() < 0.0d) { y += (int) halfFigureHeight; } g2d.translate(20, y); g2d.scale(SCALE_FUDGE_FACTOR, SCALE_FUDGE_FACTOR); theFigure.paint(g2d); g2d.dispose(); return scale; } /** * Add the motor data for a motor configuration to the table. * * @param rocket the rocket * @param motorId the motor ID to output * @param parent the parent to which the motor data will be added */ private void addMotorData(Rocket rocket, String motorId, final PdfPTable parent) { PdfPTable motorTable = new PdfPTable(8); motorTable.setWidthPercentage(68); motorTable.setHorizontalAlignment(Element.ALIGN_LEFT); final PdfPCell motorCell = ITextHelper.createCell(MOTOR, PdfPCell.BOTTOM, PrintUtilities.SMALL); final int mPad = 10; motorCell.setPaddingLeft(mPad); motorTable.addCell(motorCell); motorTable.addCell(ITextHelper.createCell(AVG_THRUST, PdfPCell.BOTTOM, PrintUtilities.SMALL)); motorTable.addCell(ITextHelper.createCell(BURN_TIME, PdfPCell.BOTTOM, PrintUtilities.SMALL)); motorTable.addCell(ITextHelper.createCell(MAX_THRUST, PdfPCell.BOTTOM, PrintUtilities.SMALL)); motorTable.addCell(ITextHelper.createCell(TOTAL_IMPULSE, PdfPCell.BOTTOM, PrintUtilities.SMALL)); motorTable.addCell(ITextHelper.createCell(THRUST_TO_WT, PdfPCell.BOTTOM, PrintUtilities.SMALL)); motorTable.addCell(ITextHelper.createCell(PROPELLANT_WT, PdfPCell.BOTTOM, PrintUtilities.SMALL)); motorTable.addCell(ITextHelper.createCell(SIZE, PdfPCell.BOTTOM, PrintUtilities.SMALL)); DecimalFormat ttwFormat = new DecimalFormat("0.00"); MassCalculator massCalc = new BasicMassCalculator(); Configuration config = new Configuration(rocket); config.setFlightConfigurationID(motorId); int totalMotorCount = 0; double totalPropMass = 0; double totalImpulse = 0; double totalTTW = 0; int stage = 0; double stageMass = 0; boolean topBorder = false; for (RocketComponent c : rocket) { if (c instanceof Stage) { config.setToStage(stage); stage++; stageMass = massCalc.getCG(config, MassCalcType.LAUNCH_MASS).weight; // Calculate total thrust-to-weight from only lowest stage motors totalTTW = 0; topBorder = true; } if (c instanceof MotorMount && ((MotorMount) c).isMotorMount()) { MotorMount mount = (MotorMount) c; if (mount.isMotorMount() && mount.getMotor(motorId) != null) { Motor motor = mount.getMotor(motorId); int motorCount = c.toAbsolute(Coordinate.NUL).length; int border = Rectangle.NO_BORDER; if (topBorder) { border = Rectangle.TOP; topBorder = false; } String name = motor.getDesignation(); if (motorCount > 1) { name += " (" + Chars.TIMES + motorCount + ")"; } final PdfPCell motorVCell = ITextHelper.createCell(name, border); motorVCell.setPaddingLeft(mPad); motorTable.addCell(motorVCell); motorTable.addCell(ITextHelper.createCell( UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(motor.getAverageThrustEstimate()), border)); motorTable.addCell(ITextHelper.createCell( UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit().toStringUnit(motor.getBurnTimeEstimate()), border)); motorTable.addCell(ITextHelper.createCell( UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(motor.getMaxThrustEstimate()), border)); motorTable.addCell(ITextHelper.createCell( UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit(motor.getTotalImpulseEstimate()), border)); double ttw = motor.getAverageThrustEstimate() / (stageMass * GRAVITY_CONSTANT); motorTable.addCell(ITextHelper.createCell( ttwFormat.format(ttw) + ":1", border)); double propMass = (motor.getLaunchCG().weight - motor.getEmptyCG().weight); motorTable.addCell(ITextHelper.createCell( UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(propMass), border)); final Unit motorUnit = UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit(); motorTable.addCell(ITextHelper.createCell(motorUnit.toString(motor.getDiameter()) + "/" + motorUnit.toString(motor.getLength()) + " " + motorUnit.toString(), border)); // Sum up total count totalMotorCount += motorCount; totalPropMass += propMass * motorCount; totalImpulse += motor.getTotalImpulseEstimate() * motorCount; totalTTW += ttw * motorCount; } } } if (totalMotorCount > 1) { int border = Rectangle.TOP; final PdfPCell motorVCell = ITextHelper.createCell("Total:", border); motorVCell.setPaddingLeft(mPad); motorTable.addCell(motorVCell); motorTable.addCell(ITextHelper.createCell("", border)); motorTable.addCell(ITextHelper.createCell("", border)); motorTable.addCell(ITextHelper.createCell("", border)); motorTable.addCell(ITextHelper.createCell( UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit(totalImpulse), border)); motorTable.addCell(ITextHelper.createCell( ttwFormat.format(totalTTW) + ":1", border)); motorTable.addCell(ITextHelper.createCell( UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(totalPropMass), border)); motorTable.addCell(ITextHelper.createCell("", border)); } PdfPCell c = new PdfPCell(motorTable); c.setBorder(PdfPCell.LEFT); c.setBorderWidthTop(0f); parent.addCell(c); config.release(); } /** * Add the flight data for a simulation configuration to the table. * * @param flight the flight data for a single simulation * @param theRocket the rocket * @param motorId a motor configuration id * @param parent the parent to which the simulation flight data will be added * @param leading the number of points for the leading */ private void addFlightData(final FlightData flight, final Rocket theRocket, final String motorId, final PdfPTable parent, int leading) { // Output the flight data if (flight != null) { try { final Unit distanceUnit = UnitGroup.UNITS_DISTANCE.getDefaultUnit(); final Unit velocityUnit = UnitGroup.UNITS_VELOCITY.getDefaultUnit(); final Unit flightTimeUnit = UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit(); PdfPTable labelTable = new PdfPTable(2); labelTable.setWidths(new int[] { 3, 2 }); final Paragraph chunk = ITextHelper.createParagraph(stripBrackets( descriptor.format(theRocket, motorId)), PrintUtilities.BOLD); chunk.setLeading(leading); chunk.setSpacingAfter(3f); document.add(chunk); final PdfPCell cell = ITextHelper.createCell(ALTITUDE, 2, 2); cell.setUseBorderPadding(false); cell.setBorderWidthTop(0f); labelTable.addCell(cell); labelTable.addCell(ITextHelper.createCell(distanceUnit.toStringUnit(flight.getMaxAltitude()), 2, 2)); labelTable.addCell(ITextHelper.createCell(FLIGHT_TIME, 2, 2)); labelTable.addCell(ITextHelper.createCell(flightTimeUnit.toStringUnit(flight.getFlightTime()), 2, 2)); labelTable.addCell(ITextHelper.createCell(TIME_TO_APOGEE, 2, 2)); labelTable.addCell(ITextHelper.createCell(flightTimeUnit.toStringUnit(flight.getTimeToApogee()), 2, 2)); labelTable.addCell(ITextHelper.createCell(OPTIMUM_DELAY, 2, 2)); labelTable.addCell(ITextHelper.createCell(flightTimeUnit.toStringUnit(flight.getBranch(0).getOptimumDelay()), 2, 2)); labelTable.addCell(ITextHelper.createCell(VELOCITY_OFF_PAD, 2, 2)); labelTable.addCell(ITextHelper.createCell(velocityUnit.toStringUnit(flight.getLaunchRodVelocity()), 2, 2)); labelTable.addCell(ITextHelper.createCell(MAX_VELOCITY, 2, 2)); labelTable.addCell(ITextHelper.createCell(velocityUnit.toStringUnit(flight.getMaxVelocity()), 2, 2)); labelTable.addCell(ITextHelper.createCell(DEPLOYMENT_VELOCITY, 2, 2)); labelTable.addCell(ITextHelper.createCell(velocityUnit.toStringUnit(flight.getDeploymentVelocity()), 2, 2)); labelTable.addCell(ITextHelper.createCell(LANDING_VELOCITY, 2, 2)); labelTable.addCell(ITextHelper.createCell(velocityUnit.toStringUnit(flight.getGroundHitVelocity()), 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) { log.error("Could not add flight data to document.", e); } } } /** * Locate the simulation based on the motor id. Copy the simulation and execute it, then return the resulting * flight data. * * @param motorId the motor id corresponding to the simulation to find * @param simulations the list of simulations currently associated with the rocket * * @return the flight data from the simulation for the specified motor id, or null if not found */ private FlightData findSimulation(final String motorId, List<Simulation> simulations) { // Perform flight simulation FlightData flight = null; try { for (int i = 0; i < simulations.size(); i++) { Simulation simulation = simulations.get(i); if (Utils.equals(simulation.getOptions().getMotorConfigurationID(), motorId)) { simulation = simulation.copy(); simulation.simulate(); flight = simulation.getSimulatedData(); break; } } } catch (SimulationException e1) { // Ignore } return flight; } /** * 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("]", ""); } }