f0be174dc002d3c5e9dafe6aa1de1d72f34c870c
[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.*;
13 import java.awt.*;
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;
23 import java.util.Map;
24
25 /**
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.
31  * <p/>
32  */
33 public class FinMarkingGuide extends JPanel {
34
35     /**
36      * The stroke of normal lines.
37      */
38     private final static BasicStroke thinStroke = new BasicStroke(1.0f);
39
40     /**
41      * The size of the arrow in points.
42      */
43     private static final int ARROW_SIZE = 10;
44
45     /**
46      * The default guide width in inches.
47      */
48     public final static double DEFAULT_GUIDE_WIDTH = 3d;
49
50     /**
51      * 2 PI radians (represents a circle).
52      */
53     public final static double TWO_PI = 2 * Math.PI;
54
55     /**
56      * The I18N translator.
57      */
58     private static final Translator trans = Application.getTranslator();
59
60     /**
61      * The margin.
62      */
63     private static final int MARGIN = (int) PrintUnit.INCHES.toPoints(0.25f);
64
65     /**
66      * The height (circumference) of the biggest body tube with a finset.
67      */
68     private int maxHeight = 0;
69
70     /**
71      * A map of body tubes, to a list of components that contains finsets and launch lugs.
72      */
73     private Map<BodyTube, java.util.List<ExternalComponent>> markingGuideItems;
74
75     /**
76      * Constructor.
77      *
78      * @param rocket the rocket instance
79      */
80     public FinMarkingGuide(Rocket rocket) {
81         super(false);
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);
86     }
87
88     /**
89      * Initialize the marking guide class by iterating over a rocket and finding all finsets.
90      *
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.
93      */
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;
98         int totalHeight = 0;
99         int iterationHeight = 0;
100         int count = 0;
101
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);
111
112                     double radius = current.getOuterRadius();
113                     int circumferenceInPoints = (int) PrintUnit.METERS.toPoints(radius * TWO_PI);
114
115                     // Find the biggest body tube circumference.
116                     if (iterationHeight < (circumferenceInPoints + MARGIN)) {
117                         iterationHeight = circumferenceInPoints + MARGIN;
118                     }
119                     //At most, two marking guides horizontally.  After that, move down and back to the left margin.
120                     count++;
121                     if (count % 2 == 0) {
122                         totalHeight += iterationHeight;
123                         iterationHeight = 0;
124                     }
125                 }
126                 if (list != null) {
127                     list.add((ExternalComponent) next);
128                 }
129             }
130         }
131         maxHeight = totalHeight + iterationHeight;
132         return results;
133     }
134
135     /**
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.
138      *
139      * @return an awt image of the fin marking guide
140      */
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();
148         // Draw graphics
149         g2d.setBackground(Color.white);
150         g2d.clearRect(0, 0, width, height);
151         paintComponent(g2d);
152         // Graphics context no longer needed so dispose it
153         g2d.dispose();
154         return bufferedImage;
155     }
156
157     /**
158      * <pre>
159      *   ---------------------- Page Edge --------------------------------------------------------
160      *   |                                        ^
161      *   |                                        |
162      *   |
163      *   |                                        y
164      *   |
165      *   |                                        |
166      *   P                                        v
167      *   a      ---                 +----------------------------+  ------------
168      *   g<------^-- x ------------>+                            +       ^
169      *   e       |                  +                            +       |
170      *           |                  +                            +     baseYOffset
171      *   E       |                  +                            +       v
172      *   d       |                  +<----------Fin------------->+ -------------
173      *   g       |                  +                            +
174      *   e       |                  +                            +
175      *   |       |                  +                            +
176      *   |       |                  +                            +
177      *   |       |                  +                            +   baseSpacing
178      *   |       |                  +                            +
179      *   |       |                  +                            +
180      *   |       |                  +                            +
181      *   |       |                  +                            +
182      *   |       |                  +<----------Fin------------->+  --------------
183      *   |       |                  +                            +
184      *   | circumferenceInPoints    +                            +
185      *   |       |                  +                            +
186      *   |       |                  +                            +
187      *   |       |                  +                            +    baseSpacing
188      *   |       |                  +<------Launch Lug --------->+           -----
189      *   |       |                  +                            +                 \
190      *   |       |                  +                            +                 + yLLOffset
191      *   |       |                  +                            +                 /
192      *   |       |                  +<----------Fin------------->+  --------------
193      *   |       |                  +                            +       ^
194      *   |       |                  +                            +       |
195      *   |       |                  +                            +    baseYOffset
196      *   |       v                  +                            +       v
197      *   |      ---                 +----------------------------+  --------------
198      *
199      *                              |<-------- width ----------->|
200      *
201      * yLLOffset is computed from the difference between the base rotation of the fin and the radial direction of the lug.
202      *
203      * Note: There is a current limitation that a tube with multiple launch lugs may not render the lug lines correctly.
204      * </pre>
205      *
206      * @param g the Graphics context
207      */
208     @Override
209     public void paintComponent(Graphics g) {
210         super.paintComponent(g);
211         Graphics2D g2 = (Graphics2D) g;
212         paintFinMarkingGuide(g2);
213     }
214
215     private void paintFinMarkingGuide(Graphics2D g2) {
216         g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
217                 RenderingHints.VALUE_ANTIALIAS_ON);
218
219         g2.setColor(Color.BLACK);
220         g2.setStroke(thinStroke);
221         int x = MARGIN;
222         int y = MARGIN;
223
224         int width = (int) PrintUnit.INCHES.toPoints(DEFAULT_GUIDE_WIDTH);
225
226         int column = 0;
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)) {
232
233                 drawMarkingGuide(g2, x, y, (int) (circumferenceInPoints), width);
234
235                 //Sort so that fins always precede lugs
236                 sort(componentList);
237
238                 boolean hasMultipleComponents = componentList.size() > 1;
239
240                 double baseSpacing = 0d;
241                 double baseYOrigin = 0;
242                 double finRadial = 0d;
243                 int yFirstFin = y;
244                 int yLastFin = y;
245                 boolean firstFinSet = true;
246
247                 //fin1: 42  fin2: 25
248                 for (ExternalComponent externalComponent : componentList) {
249                     if (externalComponent instanceof FinSet) {
250                         FinSet fins = (FinSet) externalComponent;
251                         int finCount = fins.getFinCount();
252                         int offset = 0;
253                         baseSpacing = (circumferenceInPoints / finCount);
254                         double baseRotation = fins.getBaseRotation();
255                         if (!firstFinSet) {
256                             //Adjust the rotation to a positive number.
257                             while (baseRotation < 0) {
258                                 baseRotation += TWO_PI / finCount;
259                             }
260                             offset = computeYOffset(y, circumferenceInPoints, baseSpacing, baseYOrigin, finRadial, baseRotation);
261                         } else {
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;
266                             firstFinSet = false;
267                         }
268                         yFirstFin = y;
269                         yLastFin = y;
270                         finRadial = baseRotation;
271
272                         //Draw the fin marking lines.
273                         for (int fin = 0; fin < finCount; fin++) {
274                             if (fin > 0) {
275                                 offset += baseSpacing;
276                                 yLastFin = offset;
277                             } else {
278                                 yFirstFin = offset;
279                             }
280                             drawDoubleArrowLine(g2, x, offset, x + width, offset);
281                          //   if (hasMultipleComponents) {
282                                 g2.drawString(externalComponent.getName(), x + (width / 3), offset - 2);
283                          //   }
284                         }
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.
291                         if (yLLOffset < 0) {
292                             yLLOffset = yLLOffset * circumferenceInPoints + yLastFin;
293                         } else {
294                             yLLOffset = yLLOffset * circumferenceInPoints + yFirstFin;
295                         }
296                         drawDoubleArrowLine(g2, x, (int) yLLOffset, x + width, (int) yLLOffset);
297                         g2.drawString(lug.getName(), x + (width / 3), (int) yLLOffset - 2);
298
299                     }
300                 }
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);
304                 }
305
306                 //At most, two marking guides horizontally.  After that, move down and back to the left margin.
307                 column++;
308                 if (column % 2 == 0) {
309                     x = MARGIN;
310                     y += circumferenceInPoints + MARGIN;
311                 } else {
312                     x += MARGIN + width;
313                 }
314             }
315         }
316     }
317
318     /**
319      * Compute the y offset for the next fin line.
320      *
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
328      */
329     private int computeYOffset(int y, double circumferenceInPoints, double baseSpacing, double baseYOrigin, double prevBaseRotation, double baseRotation) {
330         int offset;
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;
337         } else {
338             offset = (int) (finRadialDifference * circumferenceInPoints - baseYOrigin) + y;
339         }
340         return offset;
341     }
342
343     /**
344      * Determines if the list contains a FinSet.
345      *
346      * @param list a list of ExternalComponent
347      * @return true if the list contains at least one FinSet
348      */
349     private boolean hasFins(List<ExternalComponent> list) {
350         for (ExternalComponent externalComponent : list) {
351             if (externalComponent instanceof FinSet) {
352                 return true;
353             }
354         }
355         return false;
356     }
357
358     /**
359      * Sort a list of ExternalComponent in-place. Forces FinSets to precede Launch Lugs.
360      *
361      * @param componentList a list of ExternalComponent
362      */
363     private void sort(List<ExternalComponent> componentList) {
364         Collections.sort(componentList, new Comparator<ExternalComponent>() {
365             @Override
366             public int compare(ExternalComponent o1, ExternalComponent o2) {
367                 if (o1 instanceof FinSet) {
368                     return -1;
369                 }
370                 if (o2 instanceof FinSet) {
371                     return 1;
372                 }
373                 return 0;
374             }
375         });
376     }
377
378     /**
379      * Draw the marking guide outline.
380      *
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
386      */
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);
393         outline.closePath();
394         g2.draw(outline);
395
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;
399         //Upper left
400         g2.drawLine(x + fromEdge, y, x + fromEdge, y + tickLength);
401         //Upper right
402         g2.drawLine(x + width - fromEdge, y, x + width - fromEdge, y + tickLength);
403         //Lower left
404         g2.drawLine(x + fromEdge, y + length - tickLength, x + fromEdge, y + length);
405         //Lower right
406         g2.drawLine(x + width - fromEdge, y + length - tickLength, x + width - fromEdge, y + length);
407     }
408
409     /**
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).
412      *
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
419      */
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;
427         }
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);
433     }
434
435     /**
436      * Draw a horizontal line with arrows on both endpoints.  Depicts a fin alignment.
437      *
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
443      */
444     void drawDoubleArrowLine(Graphics2D g2, int x1, int y1, int x2, int y2) {
445         int len = x2 - x1;
446
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);
450
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);
453     }
454
455
456 }