1 package net.sf.openrocket.gui.print;
3 import net.sf.openrocket.l10n.Translator;
4 import net.sf.openrocket.rocketcomponent.BodyTube;
5 import net.sf.openrocket.rocketcomponent.ExternalComponent;
6 import net.sf.openrocket.rocketcomponent.FinSet;
7 import net.sf.openrocket.rocketcomponent.LaunchLug;
8 import net.sf.openrocket.rocketcomponent.Rocket;
9 import net.sf.openrocket.rocketcomponent.RocketComponent;
10 import net.sf.openrocket.startup.Application;
12 import javax.swing.JPanel;
13 import java.awt.BasicStroke;
14 import java.awt.Color;
15 import java.awt.Graphics;
16 import java.awt.Graphics2D;
17 import java.awt.Image;
18 import java.awt.RenderingHints;
19 import java.awt.geom.GeneralPath;
20 import java.awt.geom.Path2D;
21 import java.awt.image.BufferedImage;
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.Comparator;
25 import java.util.Iterator;
26 import java.util.LinkedHashMap;
27 import java.util.List;
31 * This is the core Swing representation of a fin marking guide. It can handle multiple fin sets on the same or
32 * different body tubes. One marking guide will be created for any body tube that has a finset. If a tube has multiple
33 * finsets, then they are combined onto one marking guide. It also includes launch lug marking line(s) if lugs are
34 * present. If (and only if) a launch lug exists, then the word 'Front' is affixed to the leading edge of the guide to
38 public class FinMarkingGuide extends JPanel {
41 * The stroke of normal lines.
43 private final static BasicStroke thinStroke = new BasicStroke(1.0f);
46 * The size of the arrow in points.
48 private static final int ARROW_SIZE = 10;
51 * Typical thickness of a piece of printer paper (~20-24 lb paper). Wrapping paper around a tube results in the
52 * radius being increased by the thickness of the paper. The smaller the tube, the more pronounced this becomes as a
53 * percentage of circumference. Using 1/10mm as an approximation here.
55 private static final double PAPER_THICKNESS_IN_METERS = PrintUnit.MILLIMETERS.toMeters(0.1d);
58 * The default guide width in inches.
60 public final static double DEFAULT_GUIDE_WIDTH = 3d;
63 * 2 PI radians (represents a circle).
65 public final static double TWO_PI = 2 * Math.PI;
68 * The I18N translator.
70 private static final Translator trans = Application.getTranslator();
75 private static final int MARGIN = (int) PrintUnit.INCHES.toPoints(0.25f);
78 * The height (circumference) of the biggest body tube with a finset.
80 private int maxHeight = 0;
83 * A map of body tubes, to a list of components that contains finsets and launch lugs.
85 private Map<BodyTube, java.util.List<ExternalComponent>> markingGuideItems;
90 * @param rocket the rocket instance
92 public FinMarkingGuide(Rocket rocket) {
94 setBackground(Color.white);
95 markingGuideItems = init(rocket);
96 //Max of 2 drawing guides horizontally per page.
97 setSize((int) PrintUnit.INCHES.toPoints(DEFAULT_GUIDE_WIDTH) * 2 + 3 * MARGIN, maxHeight);
101 * Initialize the marking guide class by iterating over a rocket and finding all finsets.
103 * @param component the root rocket component - this is iterated to find all finset and launch lugs
105 * @return a map of body tubes to lists of finsets and launch lugs.
107 private Map<BodyTube, java.util.List<ExternalComponent>> init(Rocket component) {
108 Iterator<RocketComponent> iter = component.iterator(false);
109 Map<BodyTube, java.util.List<ExternalComponent>> results = new LinkedHashMap<BodyTube, List<ExternalComponent>>();
110 BodyTube current = null;
112 int iterationHeight = 0;
115 while (iter.hasNext()) {
116 RocketComponent next = iter.next();
117 if (next instanceof BodyTube) {
118 current = (BodyTube) next;
120 else if (next instanceof FinSet || next instanceof LaunchLug) {
121 java.util.List<ExternalComponent> list = results.get(current);
122 if (list == null && current != null) {
123 list = new ArrayList<ExternalComponent>();
124 results.put(current, list);
126 double radius = current.getOuterRadius();
127 int circumferenceInPoints = (int) PrintUnit.METERS.toPoints(radius * TWO_PI);
129 // Find the biggest body tube circumference.
130 if (iterationHeight < (circumferenceInPoints + MARGIN)) {
131 iterationHeight = circumferenceInPoints + MARGIN;
133 //At most, two marking guides horizontally. After that, move down and back to the left margin.
135 if (count % 2 == 0) {
136 totalHeight += iterationHeight;
141 list.add((ExternalComponent) next);
145 maxHeight = totalHeight + iterationHeight;
150 * Returns a generated image of the fin marking guide. May then be used wherever AWT images can be used, or
151 * converted to another image/picture format and used accordingly.
153 * @return an awt image of the fin marking guide
155 public Image createImage() {
156 int width = getWidth() + 25;
157 int height = getHeight() + 25;
158 // Create a buffered image in which to draw
159 BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
160 // Create a graphics context on the buffered image
161 Graphics2D g2d = bufferedImage.createGraphics();
163 g2d.setBackground(Color.white);
164 g2d.clearRect(0, 0, width, height);
166 // Graphics context no longer needed so dispose it
168 return bufferedImage;
173 * ---------------------- Page Edge --------------------------------------------------------
181 * a --- +----------------------------+ ------------
182 * g<------^-- x ------------>+ + ^
186 * d | +<----------Fin------------->+ -------------
191 * | | + + baseSpacing
196 * | | +<----------Fin------------->+ --------------
198 * | circumferenceInPoints + +
201 * | | + + baseSpacing
202 * | | +<------Launch Lug --------->+ -----
204 * | | + + + yLLOffset
206 * | | +<----------Fin------------->+ --------------
209 * | | + + baseYOffset
211 * | --- +----------------------------+ --------------
213 * |<-------- width ----------->|
215 * yLLOffset is computed from the difference between the base rotation of the fin and the radial direction of the
218 * Note: There is a current limitation that a tube with multiple launch lugs may not render the lug lines
222 * @param g the Graphics context
225 public void paintComponent(Graphics g) {
226 super.paintComponent(g);
227 Graphics2D g2 = (Graphics2D) g;
228 paintFinMarkingGuide(g2);
231 private void paintFinMarkingGuide(Graphics2D g2) {
232 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
233 RenderingHints.VALUE_ANTIALIAS_ON);
235 g2.setColor(Color.BLACK);
236 g2.setStroke(thinStroke);
240 int width = (int) PrintUnit.INCHES.toPoints(DEFAULT_GUIDE_WIDTH);
244 for (BodyTube next : markingGuideItems.keySet()) {
245 double circumferenceInPoints = PrintUnit.METERS.toPoints((next.getOuterRadius() + PAPER_THICKNESS_IN_METERS) *
247 List<ExternalComponent> componentList = markingGuideItems.get(next);
248 //Don't draw the lug if there are no fins.
249 if (hasFins(componentList)) {
251 drawMarkingGuide(g2, x, y, (int) Math.ceil(circumferenceInPoints), width);
253 //Sort so that fins always precede lugs
256 boolean hasMultipleComponents = componentList.size() > 1;
258 double baseSpacing = 0d;
259 double baseYOrigin = 0;
260 double finRadial = 0d;
263 boolean firstFinSet = true;
266 for (ExternalComponent externalComponent : componentList) {
267 if (externalComponent instanceof FinSet) {
268 FinSet fins = (FinSet) externalComponent;
269 int finCount = fins.getFinCount();
271 baseSpacing = (circumferenceInPoints / finCount);
272 double baseRotation = fins.getBaseRotation();
274 //Adjust the rotation to a positive number.
275 while (baseRotation < 0) {
276 baseRotation += TWO_PI / finCount;
278 offset = computeYOffset(y, circumferenceInPoints, baseSpacing, baseYOrigin, finRadial, baseRotation);
281 //baseYOrigin is the distance from the top of the marking guide to the first fin of the first fin set.
282 //This measurement is used to base all subsequent finsets and lugs off of.
283 baseYOrigin = baseSpacing / 2;
284 offset = (int) (baseYOrigin) + y;
289 finRadial = baseRotation;
291 //Draw the fin marking lines.
292 for (int fin = 0; fin < finCount; fin++) {
294 offset += baseSpacing;
300 drawDoubleArrowLine(g2, x, offset, x + width, offset);
301 // if (hasMultipleComponents) {
302 g2.drawString(externalComponent.getName(), x + (width / 3), offset - 2);
306 else if (externalComponent instanceof LaunchLug) {
307 LaunchLug lug = (LaunchLug) externalComponent;
308 double yLLOffset = (lug.getRadialDirection() - finRadial) / TWO_PI;
309 //The placement of the lug line must respect the boundary of the outer marking guide. In order
310 //to do that, place it above or below either the top or bottom fin line, based on the difference
311 //between their rotational directions.
313 yLLOffset = yLLOffset * circumferenceInPoints + yLastFin;
316 yLLOffset = yLLOffset * circumferenceInPoints + yFirstFin;
318 drawDoubleArrowLine(g2, x, (int) yLLOffset, x + width, (int) yLLOffset);
319 g2.drawString(lug.getName(), x + (width / 3), (int) yLLOffset - 2);
323 //Only if the tube has a lug or multiple finsets does the orientation of the marking guide matter. So print 'Front'.
324 if (hasMultipleComponents) {
325 drawFrontIndication(g2, x, y, (int) baseSpacing, (int) circumferenceInPoints, width);
328 //At most, two marking guides horizontally. After that, move down and back to the left margin.
330 if (column % 2 == 0) {
332 y += circumferenceInPoints + MARGIN;
342 * Compute the y offset for the next fin line.
344 * @param y the top margin
345 * @param circumferenceInPoints the circumference (height) of the guide
346 * @param baseSpacing the circumference / fin count
347 * @param baseYOrigin the offset from the top of the guide to the first fin of the first fin set drawn
348 * @param prevBaseRotation the rotation of the previous finset
349 * @param baseRotation the rotation of the current finset
351 * @return number of points from the top of the marking guide to the line to be drawn
353 private int computeYOffset(int y, double circumferenceInPoints, double baseSpacing, double baseYOrigin, double prevBaseRotation, double baseRotation) {
355 double finRadialDifference = (baseRotation - prevBaseRotation) / TWO_PI;
356 //If the fin line would be off the top of the marking guide, then readjust.
357 if (baseYOrigin + finRadialDifference * circumferenceInPoints < 0) {
358 offset = (int) (baseYOrigin + baseSpacing + finRadialDifference * circumferenceInPoints) + y;
360 else if (baseYOrigin - finRadialDifference * circumferenceInPoints > 0) {
361 offset = (int) (finRadialDifference * circumferenceInPoints + baseYOrigin) + y;
364 offset = (int) (finRadialDifference * circumferenceInPoints - baseYOrigin) + y;
370 * Determines if the list contains a FinSet.
372 * @param list a list of ExternalComponent
374 * @return true if the list contains at least one FinSet
376 private boolean hasFins(List<ExternalComponent> list) {
377 for (ExternalComponent externalComponent : list) {
378 if (externalComponent instanceof FinSet) {
386 * Sort a list of ExternalComponent in-place. Forces FinSets to precede Launch Lugs.
388 * @param componentList a list of ExternalComponent
390 private void sort(List<ExternalComponent> componentList) {
391 Collections.sort(componentList, new Comparator<ExternalComponent>() {
393 public int compare(ExternalComponent o1, ExternalComponent o2) {
394 if (o1 instanceof FinSet) {
397 if (o2 instanceof FinSet) {
406 * Draw the marking guide outline.
408 * @param g2 the graphics context
409 * @param x the starting x coordinate
410 * @param y the starting y coordinate
411 * @param length the length, or height, in print units of the marking guide; should be equivalent to the outer tube
413 * @param width the width of the marking guide in print units; somewhat arbitrary
415 private void drawMarkingGuide(Graphics2D g2, int x, int y, int length, int width) {
416 Path2D outline = new Path2D.Float(GeneralPath.WIND_EVEN_ODD, 4);
417 outline.moveTo(x, y);
418 outline.lineTo(width + x, y);
419 outline.lineTo(width + x, length + y);
420 outline.lineTo(x, length + y);
424 //Draw tick marks for alignment, 1/4 of the width in from either edge
425 int fromEdge = (width) / 4;
426 final int tickLength = 8;
428 g2.drawLine(x + fromEdge, y, x + fromEdge, y + tickLength);
430 g2.drawLine(x + width - fromEdge, y, x + width - fromEdge, y + tickLength);
432 g2.drawLine(x + fromEdge, y + length - tickLength, x + fromEdge, y + length);
434 g2.drawLine(x + width - fromEdge, y + length - tickLength, x + width - fromEdge, y + length);
438 * Draw a vertical string indicating the front of the rocket. This is necessary when a launch lug exists to give
439 * proper orientation of the guide (assuming that the lug is asymmetrically positioned with respect to a fin).
441 * @param g2 the graphics context
442 * @param x the starting x coordinate
443 * @param y the starting y coordinate
444 * @param spacing the space between fin lines
445 * @param length the length, or height, in print units of the marking guide; should be equivalent to the outer tube
447 * @param width the width of the marking guide in print units; somewhat arbitrary
449 private void drawFrontIndication(Graphics2D g2, int x, int y, int spacing, int length, int width) {
450 //The magic numbers here are fairly arbitrary. They're chosen in a manner that best positions 'Front' to be
451 //readable, without going to complex string layout prediction logic.
452 int rotateX = x + width - 16;
453 int rotateY = y + (int) (spacing * 1.5) + 20;
454 if (rotateY > y + length + 14) {
455 rotateY = y + length / 2 - 10;
457 g2.translate(rotateX, rotateY);
458 g2.rotate(Math.PI / 2);
459 g2.drawString(trans.get("FinMarkingGuide.lbl.Front"), 0, 0);
460 g2.rotate(-Math.PI / 2);
461 g2.translate(-rotateX, -rotateY);
465 * Draw a horizontal line with arrows on both endpoints. Depicts a fin alignment.
467 * @param g2 the graphics context
468 * @param x1 the starting x coordinate
469 * @param y1 the starting y coordinate
470 * @param x2 the ending x coordinate
471 * @param y2 the ending y coordinate
473 void drawDoubleArrowLine(Graphics2D g2, int x1, int y1, int x2, int y2) {
476 g2.drawLine(x1, y1, x1 + len, y2);
477 g2.fillPolygon(new int[]{x1 + len, x1 + len - ARROW_SIZE, x1 + len - ARROW_SIZE, x1 + len},
478 new int[]{y2, y2 - ARROW_SIZE / 2, y2 + ARROW_SIZE / 2, y2}, 4);
480 g2.fillPolygon(new int[]{x1, x1 + ARROW_SIZE, x1 + ARROW_SIZE, x1},
481 new int[]{y1, y1 - ARROW_SIZE / 2, y1 + ARROW_SIZE / 2, y1}, 4);