create changelog entry
[debian/openrocket] / core / src / net / sf / openrocket / gui / print / FinMarkingGuide.java
1 package net.sf.openrocket.gui.print;
2
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;
11
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;
28 import java.util.Map;
29
30 /**
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
35  * give orientation.
36  * <p/>
37  */
38 public class FinMarkingGuide extends JPanel {
39
40     /**
41      * The stroke of normal lines.
42      */
43     private final static BasicStroke thinStroke = new BasicStroke(1.0f);
44
45     /**
46      * The size of the arrow in points.
47      */
48     private static final int ARROW_SIZE = 10;
49
50     /**
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.
54      */
55     private static final double PAPER_THICKNESS_IN_METERS = PrintUnit.MILLIMETERS.toMeters(0.1d);
56
57     /**
58      * The default guide width in inches.
59      */
60     public final static double DEFAULT_GUIDE_WIDTH = 3d;
61
62     /**
63      * 2 PI radians (represents a circle).
64      */
65     public final static double TWO_PI = 2 * Math.PI;
66
67     /**
68      * The I18N translator.
69      */
70     private static final Translator trans = Application.getTranslator();
71
72     /**
73      * The margin.
74      */
75     private static final int MARGIN = (int) PrintUnit.INCHES.toPoints(0.25f);
76
77     /**
78      * The height (circumference) of the biggest body tube with a finset.
79      */
80     private int maxHeight = 0;
81
82     /**
83      * A map of body tubes, to a list of components that contains finsets and launch lugs.
84      */
85     private Map<BodyTube, java.util.List<ExternalComponent>> markingGuideItems;
86
87     /**
88      * Constructor.
89      *
90      * @param rocket the rocket instance
91      */
92     public FinMarkingGuide(Rocket rocket) {
93         super(false);
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);
98     }
99
100     /**
101      * Initialize the marking guide class by iterating over a rocket and finding all finsets.
102      *
103      * @param component the root rocket component - this is iterated to find all finset and launch lugs
104      *
105      * @return a map of body tubes to lists of finsets and launch lugs.
106      */
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;
111         int totalHeight = 0;
112         int iterationHeight = 0;
113         int count = 0;
114
115         while (iter.hasNext()) {
116             RocketComponent next = iter.next();
117             if (next instanceof BodyTube) {
118                 current = (BodyTube) next;
119             }
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);
125
126                     double radius = current.getOuterRadius();
127                     int circumferenceInPoints = (int) PrintUnit.METERS.toPoints(radius * TWO_PI);
128
129                     // Find the biggest body tube circumference.
130                     if (iterationHeight < (circumferenceInPoints + MARGIN)) {
131                         iterationHeight = circumferenceInPoints + MARGIN;
132                     }
133                     //At most, two marking guides horizontally.  After that, move down and back to the left margin.
134                     count++;
135                     if (count % 2 == 0) {
136                         totalHeight += iterationHeight;
137                         iterationHeight = 0;
138                     }
139                 }
140                 if (list != null) {
141                     list.add((ExternalComponent) next);
142                 }
143             }
144         }
145         maxHeight = totalHeight + iterationHeight;
146         return results;
147     }
148
149     /**
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.
152      *
153      * @return an awt image of the fin marking guide
154      */
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();
162         // Draw graphics
163         g2d.setBackground(Color.white);
164         g2d.clearRect(0, 0, width, height);
165         paintComponent(g2d);
166         // Graphics context no longer needed so dispose it
167         g2d.dispose();
168         return bufferedImage;
169     }
170
171     /**
172      * <pre>
173      *   ---------------------- Page Edge --------------------------------------------------------
174      *   |                                        ^
175      *   |                                        |
176      *   |
177      *   |                                        y
178      *   |
179      *   |                                        |
180      *   P                                        v
181      *   a      ---                 +----------------------------+  ------------
182      *   g<------^-- x ------------>+                            +       ^
183      *   e       |                  +                            +       |
184      *           |                  +                            +     baseYOffset
185      *   E       |                  +                            +       v
186      *   d       |                  +<----------Fin------------->+ -------------
187      *   g       |                  +                            +
188      *   e       |                  +                            +
189      *   |       |                  +                            +
190      *   |       |                  +                            +
191      *   |       |                  +                            +   baseSpacing
192      *   |       |                  +                            +
193      *   |       |                  +                            +
194      *   |       |                  +                            +
195      *   |       |                  +                            +
196      *   |       |                  +<----------Fin------------->+  --------------
197      *   |       |                  +                            +
198      *   | circumferenceInPoints    +                            +
199      *   |       |                  +                            +
200      *   |       |                  +                            +
201      *   |       |                  +                            +    baseSpacing
202      *   |       |                  +<------Launch Lug --------->+           -----
203      *   |       |                  +                            +                 \
204      *   |       |                  +                            +                 + yLLOffset
205      *   |       |                  +                            +                 /
206      *   |       |                  +<----------Fin------------->+  --------------
207      *   |       |                  +                            +       ^
208      *   |       |                  +                            +       |
209      *   |       |                  +                            +    baseYOffset
210      *   |       v                  +                            +       v
211      *   |      ---                 +----------------------------+  --------------
212      *
213      *                              |<-------- width ----------->|
214      *
215      * yLLOffset is computed from the difference between the base rotation of the fin and the radial direction of the
216      * lug.
217      *
218      * Note: There is a current limitation that a tube with multiple launch lugs may not render the lug lines
219      * correctly.
220      * </pre>
221      *
222      * @param g the Graphics context
223      */
224     @Override
225     public void paintComponent(Graphics g) {
226         super.paintComponent(g);
227         Graphics2D g2 = (Graphics2D) g;
228         paintFinMarkingGuide(g2);
229     }
230
231     private void paintFinMarkingGuide(Graphics2D g2) {
232         g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
233                 RenderingHints.VALUE_ANTIALIAS_ON);
234
235         g2.setColor(Color.BLACK);
236         g2.setStroke(thinStroke);
237         int x = MARGIN;
238         int y = MARGIN;
239
240         int width = (int) PrintUnit.INCHES.toPoints(DEFAULT_GUIDE_WIDTH);
241
242         int column = 0;
243
244         for (BodyTube next : markingGuideItems.keySet()) {
245             double circumferenceInPoints = PrintUnit.METERS.toPoints((next.getOuterRadius() + PAPER_THICKNESS_IN_METERS) *
246                     TWO_PI);
247             List<ExternalComponent> componentList = markingGuideItems.get(next);
248             //Don't draw the lug if there are no fins.
249             if (hasFins(componentList)) {
250
251                 drawMarkingGuide(g2, x, y, (int) Math.ceil(circumferenceInPoints), width);
252
253                 //Sort so that fins always precede lugs
254                 sort(componentList);
255
256                 boolean hasMultipleComponents = componentList.size() > 1;
257
258                 double baseSpacing = 0d;
259                 double baseYOrigin = 0;
260                 double finRadial = 0d;
261                 int yFirstFin = y;
262                 int yLastFin = y;
263                 boolean firstFinSet = true;
264
265                 //fin1: 42  fin2: 25
266                 for (ExternalComponent externalComponent : componentList) {
267                     if (externalComponent instanceof FinSet) {
268                         FinSet fins = (FinSet) externalComponent;
269                         int finCount = fins.getFinCount();
270                         int offset = 0;
271                         baseSpacing = (circumferenceInPoints / finCount);
272                         double baseRotation = fins.getBaseRotation();
273                         if (!firstFinSet) {
274                             //Adjust the rotation to a positive number.
275                             while (baseRotation < 0) {
276                                 baseRotation += TWO_PI / finCount;
277                             }
278                             offset = computeYOffset(y, circumferenceInPoints, baseSpacing, baseYOrigin, finRadial, baseRotation);
279                         }
280                         else {
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;
285                             firstFinSet = false;
286                         }
287                         yFirstFin = y;
288                         yLastFin = y;
289                         finRadial = baseRotation;
290
291                         //Draw the fin marking lines.
292                         for (int fin = 0; fin < finCount; fin++) {
293                             if (fin > 0) {
294                                 offset += baseSpacing;
295                                 yLastFin = offset;
296                             }
297                             else {
298                                 yFirstFin = offset;
299                             }
300                             drawDoubleArrowLine(g2, x, offset, x + width, offset);
301                             //   if (hasMultipleComponents) {
302                             g2.drawString(externalComponent.getName(), x + (width / 3), offset - 2);
303                             //   }
304                         }
305                     }
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.
312                         if (yLLOffset < 0) {
313                             yLLOffset = yLLOffset * circumferenceInPoints + yLastFin;
314                         }
315                         else {
316                             yLLOffset = yLLOffset * circumferenceInPoints + yFirstFin;
317                         }
318                         drawDoubleArrowLine(g2, x, (int) yLLOffset, x + width, (int) yLLOffset);
319                         g2.drawString(lug.getName(), x + (width / 3), (int) yLLOffset - 2);
320
321                     }
322                 }
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);
326                 }
327
328                 //At most, two marking guides horizontally.  After that, move down and back to the left margin.
329                 column++;
330                 if (column % 2 == 0) {
331                     x = MARGIN;
332                     y += circumferenceInPoints + MARGIN;
333                 }
334                 else {
335                     x += MARGIN + width;
336                 }
337             }
338         }
339     }
340
341     /**
342      * Compute the y offset for the next fin line.
343      *
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
350      *
351      * @return number of points from the top of the marking guide to the line to be drawn
352      */
353     private int computeYOffset(int y, double circumferenceInPoints, double baseSpacing, double baseYOrigin, double prevBaseRotation, double baseRotation) {
354         int offset;
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;
359         }
360         else if (baseYOrigin - finRadialDifference * circumferenceInPoints > 0) {
361             offset = (int) (finRadialDifference * circumferenceInPoints + baseYOrigin) + y;
362         }
363         else {
364             offset = (int) (finRadialDifference * circumferenceInPoints - baseYOrigin) + y;
365         }
366         return offset;
367     }
368
369     /**
370      * Determines if the list contains a FinSet.
371      *
372      * @param list a list of ExternalComponent
373      *
374      * @return true if the list contains at least one FinSet
375      */
376     private boolean hasFins(List<ExternalComponent> list) {
377         for (ExternalComponent externalComponent : list) {
378             if (externalComponent instanceof FinSet) {
379                 return true;
380             }
381         }
382         return false;
383     }
384
385     /**
386      * Sort a list of ExternalComponent in-place. Forces FinSets to precede Launch Lugs.
387      *
388      * @param componentList a list of ExternalComponent
389      */
390     private void sort(List<ExternalComponent> componentList) {
391         Collections.sort(componentList, new Comparator<ExternalComponent>() {
392             @Override
393             public int compare(ExternalComponent o1, ExternalComponent o2) {
394                 if (o1 instanceof FinSet) {
395                     return -1;
396                 }
397                 if (o2 instanceof FinSet) {
398                     return 1;
399                 }
400                 return 0;
401             }
402         });
403     }
404
405     /**
406      * Draw the marking guide outline.
407      *
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
412      *               circumference
413      * @param width  the width of the marking guide in print units; somewhat arbitrary
414      */
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);
421         outline.closePath();
422         g2.draw(outline);
423
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;
427         //Upper left
428         g2.drawLine(x + fromEdge, y, x + fromEdge, y + tickLength);
429         //Upper right
430         g2.drawLine(x + width - fromEdge, y, x + width - fromEdge, y + tickLength);
431         //Lower left
432         g2.drawLine(x + fromEdge, y + length - tickLength, x + fromEdge, y + length);
433         //Lower right
434         g2.drawLine(x + width - fromEdge, y + length - tickLength, x + width - fromEdge, y + length);
435     }
436
437     /**
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).
440      *
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
446      *                circumference
447      * @param width   the width of the marking guide in print units; somewhat arbitrary
448      */
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;
456         }
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);
462     }
463
464     /**
465      * Draw a horizontal line with arrows on both endpoints.  Depicts a fin alignment.
466      *
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
472      */
473     void drawDoubleArrowLine(Graphics2D g2, int x1, int y1, int x2, int y2) {
474         int len = x2 - x1;
475
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);
479
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);
482     }
483 }