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;
14 import java.awt.geom.GeneralPath;
15 import java.awt.geom.Path2D;
16 import java.awt.image.BufferedImage;
17 import java.util.ArrayList;
18 import java.util.Collections;
19 import java.util.Comparator;
20 import java.util.Iterator;
21 import java.util.LinkedHashMap;
22 import java.util.List;
26 * This is the core Swing representation of a fin marking guide. It can handle multiple fin sets on the same or
27 * different body tubes. One marking guide will be created for any body tube that has a finset. If a tube has
28 * multiple finsets, then they are combined onto one marking guide. It also includes launch lug marking line(s) if lugs
29 * are present. If (and only if) a launch lug exists, then the word 'Front' is affixed to the leading edge of the
30 * guide to give orientation.
33 public class FinMarkingGuide extends JPanel {
36 * The stroke of normal lines.
38 private final static BasicStroke thinStroke = new BasicStroke(1.0f);
41 * The size of the arrow in points.
43 private static final int ARROW_SIZE = 10;
46 * The default guide width in inches.
48 public final static double DEFAULT_GUIDE_WIDTH = 3d;
51 * 2 PI radians (represents a circle).
53 public final static double TWO_PI = 2 * Math.PI;
56 * The I18N translator.
58 private static final Translator trans = Application.getTranslator();
63 private static final int MARGIN = (int) PrintUnit.INCHES.toPoints(0.25f);
66 * The height (circumference) of the biggest body tube with a finset.
68 private int maxHeight = 0;
71 * A map of body tubes, to a list of components that contains finsets and launch lugs.
73 private Map<BodyTube, java.util.List<ExternalComponent>> markingGuideItems;
78 * @param rocket the rocket instance
80 public FinMarkingGuide(Rocket rocket) {
82 setBackground(Color.white);
83 markingGuideItems = init(rocket);
84 //Max of 2 drawing guides horizontally per page.
85 setSize((int) PrintUnit.INCHES.toPoints(DEFAULT_GUIDE_WIDTH) * 2 + 3 * MARGIN, maxHeight);
89 * Initialize the marking guide class by iterating over a rocket and finding all finsets.
91 * @param component the root rocket component - this is iterated to find all finset and launch lugs
92 * @return a map of body tubes to lists of finsets and launch lugs.
94 private Map<BodyTube, java.util.List<ExternalComponent>> init(Rocket component) {
95 Iterator<RocketComponent> iter = component.iterator(false);
96 Map<BodyTube, java.util.List<ExternalComponent>> results = new LinkedHashMap<BodyTube, List<ExternalComponent>>();
97 BodyTube current = null;
99 int iterationHeight = 0;
102 while (iter.hasNext()) {
103 RocketComponent next = iter.next();
104 if (next instanceof BodyTube) {
105 current = (BodyTube) next;
106 } else if (next instanceof FinSet || next instanceof LaunchLug) {
107 java.util.List<ExternalComponent> list = results.get(current);
108 if (list == null && current != null) {
109 list = new ArrayList<ExternalComponent>();
110 results.put(current, list);
112 double radius = current.getOuterRadius();
113 int circumferenceInPoints = (int) PrintUnit.METERS.toPoints(radius * TWO_PI);
115 // Find the biggest body tube circumference.
116 if (iterationHeight < (circumferenceInPoints + MARGIN)) {
117 iterationHeight = circumferenceInPoints + MARGIN;
119 //At most, two marking guides horizontally. After that, move down and back to the left margin.
121 if (count % 2 == 0) {
122 totalHeight += iterationHeight;
127 list.add((ExternalComponent) next);
131 maxHeight = totalHeight + iterationHeight;
136 * Returns a generated image of the fin marking guide. May then be used wherever AWT images can be used, or
137 * converted to another image/picture format and used accordingly.
139 * @return an awt image of the fin marking guide
141 public Image createImage() {
142 int width = getWidth() + 25;
143 int height = getHeight() + 25;
144 // Create a buffered image in which to draw
145 BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
146 // Create a graphics context on the buffered image
147 Graphics2D g2d = bufferedImage.createGraphics();
149 g2d.setBackground(Color.white);
150 g2d.clearRect(0, 0, width, height);
152 // Graphics context no longer needed so dispose it
154 return bufferedImage;
159 * ---------------------- Page Edge --------------------------------------------------------
167 * a --- +----------------------------+ ------------
168 * g<------^-- x ------------>+ + ^
172 * d | +<----------Fin------------->+ -------------
177 * | | + + baseSpacing
182 * | | +<----------Fin------------->+ --------------
184 * | circumferenceInPoints + +
187 * | | + + baseSpacing
188 * | | +<------Launch Lug --------->+ -----
190 * | | + + + yLLOffset
192 * | | +<----------Fin------------->+ --------------
195 * | | + + baseYOffset
197 * | --- +----------------------------+ --------------
199 * |<-------- width ----------->|
201 * yLLOffset is computed from the difference between the base rotation of the fin and the radial direction of the lug.
203 * Note: There is a current limitation that a tube with multiple launch lugs may not render the lug lines correctly.
206 * @param g the Graphics context
209 public void paintComponent(Graphics g) {
210 super.paintComponent(g);
211 Graphics2D g2 = (Graphics2D) g;
212 paintFinMarkingGuide(g2);
215 private void paintFinMarkingGuide(Graphics2D g2) {
216 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
217 RenderingHints.VALUE_ANTIALIAS_ON);
219 g2.setColor(Color.BLACK);
220 g2.setStroke(thinStroke);
224 int width = (int) PrintUnit.INCHES.toPoints(DEFAULT_GUIDE_WIDTH);
227 for (BodyTube next : markingGuideItems.keySet()) {
228 double circumferenceInPoints = PrintUnit.METERS.toPoints(next.getOuterRadius() * TWO_PI);
229 List<ExternalComponent> componentList = markingGuideItems.get(next);
230 //Don't draw the lug if there are no fins.
231 if (hasFins(componentList)) {
233 drawMarkingGuide(g2, x, y, (int) (circumferenceInPoints), width);
235 //Sort so that fins always precede lugs
238 boolean hasMultipleComponents = componentList.size() > 1;
240 double baseSpacing = 0d;
241 double baseYOrigin = 0;
242 double finRadial = 0d;
245 boolean firstFinSet = true;
248 for (ExternalComponent externalComponent : componentList) {
249 if (externalComponent instanceof FinSet) {
250 FinSet fins = (FinSet) externalComponent;
251 int finCount = fins.getFinCount();
253 baseSpacing = (circumferenceInPoints / finCount);
254 double baseRotation = fins.getBaseRotation();
256 //Adjust the rotation to a positive number.
257 while (baseRotation < 0) {
258 baseRotation += TWO_PI / finCount;
260 offset = computeYOffset(y, circumferenceInPoints, baseSpacing, baseYOrigin, finRadial, baseRotation);
262 //baseYOrigin is the distance from the top of the marking guide to the first fin of the first fin set.
263 //This measurement is used to base all subsequent finsets and lugs off of.
264 baseYOrigin = baseSpacing / 2;
265 offset = (int) (baseYOrigin) + y;
270 finRadial = baseRotation;
272 //Draw the fin marking lines.
273 for (int fin = 0; fin < finCount; fin++) {
275 offset += baseSpacing;
280 drawDoubleArrowLine(g2, x, offset, x + width, offset);
281 if (hasMultipleComponents) {
282 g2.drawString(externalComponent.getName(), x + (width / 3), offset - 2);
285 } else if (externalComponent instanceof LaunchLug) {
286 LaunchLug lug = (LaunchLug) externalComponent;
287 double yLLOffset = (lug.getRadialDirection() - finRadial) / TWO_PI;
288 //The placement of the lug line must respect the boundary of the outer marking guide. In order
289 //to do that, place it above or below either the top or bottom fin line, based on the difference
290 //between their rotational directions.
292 yLLOffset = yLLOffset * circumferenceInPoints + yLastFin;
294 yLLOffset = yLLOffset * circumferenceInPoints + yFirstFin;
296 drawDoubleArrowLine(g2, x, (int) yLLOffset, x + width, (int) yLLOffset);
297 g2.drawString(lug.getName(), x + (width / 3), (int) yLLOffset - 2);
301 //Only if the tube has a lug or multiple finsets does the orientation of the marking guide matter. So print 'Front'.
302 if (hasMultipleComponents) {
303 drawFrontIndication(g2, x, y, (int) baseSpacing, (int) circumferenceInPoints, width);
306 //At most, two marking guides horizontally. After that, move down and back to the left margin.
308 if (column % 2 == 0) {
310 y += circumferenceInPoints + MARGIN;
319 * Compute the y offset for the next fin line.
321 * @param y the top margin
322 * @param circumferenceInPoints the circumference (height) of the guide
323 * @param baseSpacing the circumference / fin count
324 * @param baseYOrigin the offset from the top of the guide to the first fin of the first fin set drawn
325 * @param prevBaseRotation the rotation of the previous finset
326 * @param baseRotation the rotation of the current finset
327 * @return number of points from the top of the marking guide to the line to be drawn
329 private int computeYOffset(int y, double circumferenceInPoints, double baseSpacing, double baseYOrigin, double prevBaseRotation, double baseRotation) {
331 double finRadialDifference = (baseRotation - prevBaseRotation) / TWO_PI;
332 //If the fin line would be off the top of the marking guide, then readjust.
333 if (baseYOrigin + finRadialDifference * circumferenceInPoints < 0) {
334 offset = (int) (baseYOrigin + baseSpacing + finRadialDifference * circumferenceInPoints) + y;
335 } else if (baseYOrigin - finRadialDifference * circumferenceInPoints > 0) {
336 offset = (int) (finRadialDifference * circumferenceInPoints + baseYOrigin) + y;
338 offset = (int) (finRadialDifference * circumferenceInPoints - baseYOrigin) + y;
344 * Determines if the list contains a FinSet.
346 * @param list a list of ExternalComponent
347 * @return true if the list contains at least one FinSet
349 private boolean hasFins(List<ExternalComponent> list) {
350 for (ExternalComponent externalComponent : list) {
351 if (externalComponent instanceof FinSet) {
359 * Sort a list of ExternalComponent in-place. Forces FinSets to precede Launch Lugs.
361 * @param componentList a list of ExternalComponent
363 private void sort(List<ExternalComponent> componentList) {
364 Collections.sort(componentList, new Comparator<ExternalComponent>() {
366 public int compare(ExternalComponent o1, ExternalComponent o2) {
367 if (o1 instanceof FinSet) {
370 if (o2 instanceof FinSet) {
379 * Draw the marking guide outline.
381 * @param g2 the graphics context
382 * @param x the starting x coordinate
383 * @param y the starting y coordinate
384 * @param length the length, or height, in print units of the marking guide; should be equivalent to the outer tube circumference
385 * @param width the width of the marking guide in print units; somewhat arbitrary
387 private void drawMarkingGuide(Graphics2D g2, int x, int y, int length, int width) {
388 Path2D outline = new Path2D.Float(GeneralPath.WIND_EVEN_ODD, 4);
389 outline.moveTo(x, y);
390 outline.lineTo(width + x, y);
391 outline.lineTo(width + x, length + y);
392 outline.lineTo(x, length + y);
396 //Draw tick marks for alignment, 1/4 of the width in from either edge
397 int fromEdge = (width) / 4;
398 final int tickLength = 8;
400 g2.drawLine(x + fromEdge, y, x + fromEdge, y + tickLength);
402 g2.drawLine(x + width - fromEdge, y, x + width - fromEdge, y + tickLength);
404 g2.drawLine(x + fromEdge, y + length - tickLength, x + fromEdge, y + length);
406 g2.drawLine(x + width - fromEdge, y + length - tickLength, x + width - fromEdge, y + length);
410 * Draw a vertical string indicating the front of the rocket. This is necessary when a launch lug exists to
411 * give proper orientation of the guide (assuming that the lug is asymmetrically positioned with respect to a fin).
413 * @param g2 the graphics context
414 * @param x the starting x coordinate
415 * @param y the starting y coordinate
416 * @param spacing the space between fin lines
417 * @param length the length, or height, in print units of the marking guide; should be equivalent to the outer tube circumference
418 * @param width the width of the marking guide in print units; somewhat arbitrary
420 private void drawFrontIndication(Graphics2D g2, int x, int y, int spacing, int length, int width) {
421 //The magic numbers here are fairly arbitrary. They're chosen in a manner that best positions 'Front' to be
422 //readable, without going to complex string layout prediction logic.
423 int rotateX = x + width - 16;
424 int rotateY = y + (int) (spacing * 1.5) + 20;
425 if (rotateY > y + length + 14) {
426 rotateY = y + length / 2 - 10;
428 g2.translate(rotateX, rotateY);
429 g2.rotate(Math.PI / 2);
430 g2.drawString(trans.get("FinMarkingGuide.lbl.Front"), 0, 0);
431 g2.rotate(-Math.PI / 2);
432 g2.translate(-rotateX, -rotateY);
436 * Draw a horizontal line with arrows on both endpoints. Depicts a fin alignment.
438 * @param g2 the graphics context
439 * @param x1 the starting x coordinate
440 * @param y1 the starting y coordinate
441 * @param x2 the ending x coordinate
442 * @param y2 the ending y coordinate
444 void drawDoubleArrowLine(Graphics2D g2, int x1, int y1, int x2, int y2) {
447 g2.drawLine(x1, y1, x1 + len, y2);
448 g2.fillPolygon(new int[]{x1 + len, x1 + len - ARROW_SIZE, x1 + len - ARROW_SIZE, x1 + len},
449 new int[]{y2, y2 - ARROW_SIZE / 2, y2 + ARROW_SIZE / 2, y2}, 4);
451 g2.fillPolygon(new int[]{x1, x1 + ARROW_SIZE, x1 + ARROW_SIZE, x1},
452 new int[]{y1, y1 - ARROW_SIZE / 2, y1 + ARROW_SIZE / 2, y1}, 4);