e388b4a6f00579dc397b8e8b8c97a47a8e704732
[debian/openrocket] / src / net / sf / openrocket / rocketcomponent / FinSet.java
1 package net.sf.openrocket.rocketcomponent;
2
3 import java.util.ArrayList;
4 import java.util.Arrays;
5 import java.util.Collection;
6 import java.util.List;
7
8 import net.sf.openrocket.l10n.Translator;
9 import net.sf.openrocket.startup.Application;
10 import net.sf.openrocket.util.Coordinate;
11 import net.sf.openrocket.util.MathUtil;
12 import net.sf.openrocket.util.Transformation;
13
14
15 public abstract class FinSet extends ExternalComponent {
16         private static final Translator trans = Application.getTranslator();
17         
18         // FIXME:  converting triangular fins to freeform fails
19         
20         /**
21          * Maximum allowed cant of fins.
22          */
23         public static final double MAX_CANT = (15.0 * Math.PI / 180);
24         
25         
26         public enum CrossSection {
27                 //// Square
28                 SQUARE(trans.get("FinSet.CrossSection.SQUARE"), 1.00),
29                 //// Rounded
30                 ROUNDED(trans.get("FinSet.CrossSection.ROUNDED"), 0.99),
31                 //// Airfoil
32                 AIRFOIL(trans.get("FinSet.CrossSection.AIRFOIL"), 0.85);
33                 
34                 private final String name;
35                 private final double volume;
36                 
37                 CrossSection(String name, double volume) {
38                         this.name = name;
39                         this.volume = volume;
40                 }
41                 
42                 public double getRelativeVolume() {
43                         return volume;
44                 }
45                 
46                 @Override
47                 public String toString() {
48                         return name;
49                 }
50         }
51         
52         public enum TabRelativePosition {
53                 //// Root chord leading edge
54                 FRONT(trans.get("FinSet.TabRelativePosition.FRONT")),
55                 //// Root chord midpoint
56                 CENTER(trans.get("FinSet.TabRelativePosition.CENTER")),
57                 //// Root chord trailing edge
58                 END(trans.get("FinSet.TabRelativePosition.END"));
59                 
60                 private final String name;
61                 
62                 TabRelativePosition(String name) {
63                         this.name = name;
64                 }
65                 
66                 @Override
67                 public String toString() {
68                         return name;
69                 }
70         }
71         
72         /**
73          * Number of fins.
74          */
75         protected int fins = 3;
76         
77         /**
78          * Rotation about the x-axis by 2*PI/fins.
79          */
80         protected Transformation finRotation = Transformation.rotate_x(2 * Math.PI / fins);
81         
82         /**
83          * Rotation angle of the first fin.  Zero corresponds to the positive y-axis.
84          */
85         protected double rotation = 0;
86         
87         /**
88          * Rotation about the x-axis by angle this.rotation.
89          */
90         protected Transformation baseRotation = Transformation.rotate_x(rotation);
91         
92
93         /**
94          * Cant angle of fins.
95          */
96         protected double cantAngle = 0;
97         
98         /* Cached value: */
99         private Transformation cantRotation = null;
100         
101
102         /**
103          * Thickness of the fins.
104          */
105         protected double thickness = 0.003;
106         
107
108         /**
109          * The cross-section shape of the fins.
110          */
111         protected CrossSection crossSection = CrossSection.SQUARE;
112         
113
114         /*
115          * Fin tab properties.
116          */
117         private double tabHeight = 0;
118         private double tabLength = 0.05;
119         private double tabShift = 0;
120         private TabRelativePosition tabRelativePosition = TabRelativePosition.CENTER;
121         
122
123         // Cached fin area & CG.  Validity of both must be checked using finArea!
124         // Fin area does not include fin tabs, CG does.
125         private double finArea = -1;
126         private double finCGx = -1;
127         private double finCGy = -1;
128         
129         
130         /**
131          * New FinSet with given number of fins and given base rotation angle.
132          * Sets the component relative position to POSITION_RELATIVE_BOTTOM,
133          * i.e. fins are positioned at the bottom of the parent component.
134          */
135         public FinSet() {
136                 super(RocketComponent.Position.BOTTOM);
137         }
138         
139         
140
141         /**
142          * Return the number of fins in the set.
143          * @return The number of fins.
144          */
145         public int getFinCount() {
146                 return fins;
147         }
148         
149         /**
150          * Sets the number of fins in the set.
151          * @param n The number of fins, greater of equal to one.
152          */
153         public void setFinCount(int n) {
154                 if (fins == n)
155                         return;
156                 if (n < 1)
157                         n = 1;
158                 if (n > 8)
159                         n = 8;
160                 fins = n;
161                 finRotation = Transformation.rotate_x(2 * Math.PI / fins);
162                 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
163         }
164         
165         public Transformation getFinRotationTransformation() {
166                 return finRotation;
167         }
168         
169         /**
170          * Gets the base rotation amount of the first fin.
171          * @return The base rotation amount.
172          */
173         public double getBaseRotation() {
174                 return rotation;
175         }
176         
177         /**
178          * Sets the base rotation amount of the first fin.
179          * @param r The base rotation amount.
180          */
181         public void setBaseRotation(double r) {
182                 r = MathUtil.reduce180(r);
183                 if (MathUtil.equals(r, rotation))
184                         return;
185                 rotation = r;
186                 baseRotation = Transformation.rotate_x(rotation);
187                 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
188         }
189         
190         public Transformation getBaseRotationTransformation() {
191                 return baseRotation;
192         }
193         
194         
195
196         public double getCantAngle() {
197                 return cantAngle;
198         }
199         
200         public void setCantAngle(double cant) {
201                 cant = MathUtil.clamp(cant, -MAX_CANT, MAX_CANT);
202                 if (MathUtil.equals(cant, cantAngle))
203                         return;
204                 this.cantAngle = cant;
205                 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
206         }
207         
208         
209         public Transformation getCantRotation() {
210                 if (cantRotation == null) {
211                         if (MathUtil.equals(cantAngle, 0)) {
212                                 cantRotation = Transformation.IDENTITY;
213                         } else {
214                                 Transformation t = new Transformation(-length / 2, 0, 0);
215                                 t = Transformation.rotate_y(cantAngle).applyTransformation(t);
216                                 t = new Transformation(length / 2, 0, 0).applyTransformation(t);
217                                 cantRotation = t;
218                         }
219                 }
220                 return cantRotation;
221         }
222         
223         
224
225         public double getThickness() {
226                 return thickness;
227         }
228         
229         public void setThickness(double r) {
230                 if (thickness == r)
231                         return;
232                 thickness = Math.max(r, 0);
233                 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
234         }
235         
236         
237         public CrossSection getCrossSection() {
238                 return crossSection;
239         }
240         
241         public void setCrossSection(CrossSection cs) {
242                 if (crossSection == cs)
243                         return;
244                 crossSection = cs;
245                 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
246         }
247         
248         
249
250
251
252         @Override
253         public void setRelativePosition(RocketComponent.Position position) {
254                 super.setRelativePosition(position);
255                 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
256         }
257         
258         
259         @Override
260         public void setPositionValue(double value) {
261                 super.setPositionValue(value);
262                 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
263         }
264         
265         
266
267
268         public double getTabHeight() {
269                 return tabHeight;
270         }
271         
272         public void setTabHeight(double height) {
273                 height = MathUtil.max(height, 0);
274                 if (MathUtil.equals(this.tabHeight, height))
275                         return;
276                 this.tabHeight = height;
277                 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
278         }
279         
280         
281         public double getTabLength() {
282                 return tabLength;
283         }
284         
285         public void setTabLength(double length) {
286                 length = MathUtil.max(length, 0);
287                 if (MathUtil.equals(this.tabLength, length))
288                         return;
289                 this.tabLength = length;
290                 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
291         }
292         
293         
294         public double getTabShift() {
295                 return tabShift;
296         }
297         
298         public void setTabShift(double shift) {
299                 this.tabShift = shift;
300                 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
301         }
302         
303         
304         public TabRelativePosition getTabRelativePosition() {
305                 return tabRelativePosition;
306         }
307         
308         public void setTabRelativePosition(TabRelativePosition position) {
309                 if (this.tabRelativePosition == position)
310                         return;
311                 
312
313                 double front = getTabFrontEdge();
314                 switch (position) {
315                 case FRONT:
316                         this.tabShift = front;
317                         break;
318                 
319                 case CENTER:
320                         this.tabShift = front + tabLength / 2 - getLength() / 2;
321                         break;
322                 
323                 case END:
324                         this.tabShift = front + tabLength - getLength();
325                         break;
326                 
327                 default:
328                         throw new IllegalArgumentException("position=" + position);
329                 }
330                 this.tabRelativePosition = position;
331                 
332                 fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
333         }
334         
335         
336         /**
337          * Return the tab front edge position from the front of the fin.
338          */
339         public double getTabFrontEdge() {
340                 switch (this.tabRelativePosition) {
341                 case FRONT:
342                         return tabShift;
343                         
344                 case CENTER:
345                         return getLength() / 2 - tabLength / 2 + tabShift;
346                         
347                 case END:
348                         return getLength() - tabLength + tabShift;
349                         
350                 default:
351                         throw new IllegalStateException("tabRelativePosition=" + tabRelativePosition);
352                 }
353         }
354         
355         /**
356          * Return the tab trailing edge position *from the front of the fin*.
357          */
358         public double getTabTrailingEdge() {
359                 switch (this.tabRelativePosition) {
360                 case FRONT:
361                         return tabLength + tabShift;
362                 case CENTER:
363                         return getLength() / 2 + tabLength / 2 + tabShift;
364                         
365                 case END:
366                         return getLength() + tabShift;
367                         
368                 default:
369                         throw new IllegalStateException("tabRelativePosition=" + tabRelativePosition);
370                 }
371         }
372         
373         
374
375
376         ///////////  Calculation methods  ///////////
377         
378         /**
379          * Return the area of one side of one fin.  This does NOT include the area of
380          * the fin tab.
381          * 
382          * @return   the area of one side of one fin.
383          */
384         public double getFinArea() {
385                 if (finArea < 0)
386                         calculateAreaCG();
387                 
388                 return finArea;
389         }
390         
391         
392         /**
393          * Return the unweighted CG of a single fin.  The X-coordinate is relative to
394          * the root chord trailing edge and the Y-coordinate to the fin root chord.
395          * 
396          * @return  the unweighted CG coordinate of a single fin. 
397          */
398         public Coordinate getFinCG() {
399                 if (finArea < 0)
400                         calculateAreaCG();
401                 
402                 return new Coordinate(finCGx, finCGy, 0);
403         }
404         
405         
406
407         @Override
408         public double getComponentVolume() {
409                 return fins * (getFinArea() + tabHeight * tabLength) * thickness *
410                                 crossSection.getRelativeVolume();
411         }
412         
413         
414         @Override
415         public Coordinate getComponentCG() {
416                 if (finArea < 0)
417                         calculateAreaCG();
418                 
419                 double mass = getComponentMass(); // safe
420                 
421                 if (fins == 1) {
422                         return baseRotation.transform(
423                                         new Coordinate(finCGx, finCGy + getBodyRadius(), 0, mass));
424                 } else {
425                         return new Coordinate(finCGx, 0, 0, mass);
426                 }
427         }
428         
429         
430         private void calculateAreaCG() {
431                 Coordinate[] points = this.getFinPoints();
432                 finArea = 0;
433                 finCGx = 0;
434                 finCGy = 0;
435                 
436                 for (int i = 0; i < points.length - 1; i++) {
437                         final double x0 = points[i].x;
438                         final double x1 = points[i + 1].x;
439                         final double y0 = points[i].y;
440                         final double y1 = points[i + 1].y;
441                         
442                         double da = (y0 + y1) * (x1 - x0) / 2;
443                         finArea += da;
444                         if (Math.abs(y0 - y1) < 0.00001) {
445                                 finCGx += (x0 + x1) / 2 * da;
446                                 finCGy += y0 / 2 * da;
447                         } else {
448                                 finCGx += (x0 * (2 * y0 + y1) + x1 * (y0 + 2 * y1)) / (3 * (y0 + y1)) * da;
449                                 finCGy += (y1 + y0 * y0 / (y0 + y1)) / 3 * da;
450                         }
451                 }
452                 
453                 if (finArea < 0)
454                         finArea = 0;
455                 
456                 // Add effect of fin tabs to CG
457                 double tabArea = tabLength * tabHeight;
458                 if (!MathUtil.equals(tabArea, 0)) {
459                         
460                         double x = (getTabFrontEdge() + getTabTrailingEdge()) / 2;
461                         double y = -this.tabHeight / 2;
462                         
463                         finCGx += x * tabArea;
464                         finCGy += y * tabArea;
465                         
466                 }
467                 
468                 if ((finArea + tabArea) > 0) {
469                         finCGx /= (finArea + tabArea);
470                         finCGy /= (finArea + tabArea);
471                 } else {
472                         finCGx = (points[0].x + points[points.length - 1].x) / 2;
473                         finCGy = 0;
474                 }
475         }
476         
477         
478         /*
479          * Return an approximation of the longitudinal unitary inertia of the fin set.
480          * The process is the following:
481          * 
482          * 1. Approximate the fin with a rectangular fin
483          * 
484          * 2. The inertia of one fin is taken as the average of the moments of inertia
485          *    through its center perpendicular to the plane, and the inertia through
486          *    its center parallel to the plane
487          *    
488          * 3. If there are multiple fins, the inertia is shifted to the center of the fin
489          *    set and multiplied by the number of fins.
490          */
491         @Override
492         public double getLongitudinalUnitInertia() {
493                 double area = getFinArea();
494                 if (MathUtil.equals(area, 0))
495                         return 0;
496                 
497                 // Approximate fin with a rectangular fin
498                 // w2 and h2 are squares of the fin width and height
499                 double w = getLength();
500                 double h = getSpan();
501                 double w2, h2;
502                 
503                 if (MathUtil.equals(w * h, 0)) {
504                         w2 = area;
505                         h2 = area;
506                 } else {
507                         w2 = w * area / h;
508                         h2 = h * area / w;
509                 }
510                 
511                 double inertia = (h2 + 2 * w2) / 24;
512                 
513                 if (fins == 1)
514                         return inertia;
515                 
516                 double radius = getBodyRadius();
517                 
518                 return fins * (inertia + MathUtil.pow2(Math.sqrt(h2) + radius));
519         }
520         
521         
522         /*
523          * Return an approximation of the rotational unitary inertia of the fin set.
524          * The process is the following:
525          * 
526          * 1. Approximate the fin with a rectangular fin and calculate the inertia of the
527          *    rectangular approximate
528          *    
529          * 2. If there are multiple fins, shift the inertia center to the fin set center
530          *    and multiply with the number of fins.
531          */
532         @Override
533         public double getRotationalUnitInertia() {
534                 double area = getFinArea();
535                 if (MathUtil.equals(area, 0))
536                         return 0;
537                 
538                 // Approximate fin with a rectangular fin
539                 double w = getLength();
540                 double h = getSpan();
541                 
542                 if (MathUtil.equals(w * h, 0)) {
543                         h = Math.sqrt(area);
544                 } else {
545                         h = Math.sqrt(h * area / w);
546                 }
547                 
548                 if (fins == 1)
549                         return h * h / 12;
550                 
551                 double radius = getBodyRadius();
552                 
553                 return fins * (h * h / 12 + MathUtil.pow2(h / 2 + radius));
554         }
555         
556         
557         /**
558          * Adds the fin set's bounds to the collection.
559          */
560         @Override
561         public Collection<Coordinate> getComponentBounds() {
562                 List<Coordinate> bounds = new ArrayList<Coordinate>();
563                 double r = getBodyRadius();
564                 
565                 for (Coordinate point : getFinPoints()) {
566                         addFinBound(bounds, point.x, point.y + r);
567                 }
568                 
569                 return bounds;
570         }
571         
572         
573         /**
574          * Adds the 2d-coordinate bound (x,y) to the collection for both z-components and for
575          * all fin rotations.
576          */
577         private void addFinBound(Collection<Coordinate> set, double x, double y) {
578                 Coordinate c;
579                 int i;
580                 
581                 c = new Coordinate(x, y, thickness / 2);
582                 c = baseRotation.transform(c);
583                 set.add(c);
584                 for (i = 1; i < fins; i++) {
585                         c = finRotation.transform(c);
586                         set.add(c);
587                 }
588                 
589                 c = new Coordinate(x, y, -thickness / 2);
590                 c = baseRotation.transform(c);
591                 set.add(c);
592                 for (i = 1; i < fins; i++) {
593                         c = finRotation.transform(c);
594                         set.add(c);
595                 }
596         }
597         
598         
599
600         @Override
601         public void componentChanged(ComponentChangeEvent e) {
602                 if (e.isAerodynamicChange()) {
603                         finArea = -1;
604                         cantRotation = null;
605                 }
606         }
607         
608         
609         /**
610          * Return the radius of the BodyComponent the fin set is situated on.  Currently
611          * only supports SymmetricComponents and returns the radius at the starting point of the
612          * root chord.
613          *  
614          * @return  radius of the underlying BodyComponent or 0 if none exists.
615          */
616         public double getBodyRadius() {
617                 RocketComponent s;
618                 
619                 s = this.getParent();
620                 while (s != null) {
621                         if (s instanceof SymmetricComponent) {
622                                 double x = this.toRelative(new Coordinate(0, 0, 0), s)[0].x;
623                                 return ((SymmetricComponent) s).getRadius(x);
624                         }
625                         s = s.getParent();
626                 }
627                 return 0;
628         }
629         
630         @Override
631         public boolean allowsChildren() {
632                 return false;
633         }
634         
635         /**
636          * Allows nothing to be attached to a FinSet.
637          * 
638          * @return <code>false</code>
639          */
640         @Override
641         public boolean isCompatible(Class<? extends RocketComponent> type) {
642                 return false;
643         }
644         
645         
646
647
648         /**
649          * Return a list of coordinates defining the geometry of a single fin.  
650          * The coordinates are the XY-coordinates of points defining the shape of a single fin,
651          * where the origin is the leading root edge.  Therefore, the first point must be (0,0,0).
652          * All Z-coordinates must be zero, and the last coordinate must have Y=0.
653          * 
654          * @return  List of XY-coordinates.
655          */
656         public abstract Coordinate[] getFinPoints();
657         
658         
659         /**
660          * Return a list of coordinates defining the geometry of a single fin, including a
661          * possible fin tab.  The coordinates are the XY-coordinates of points defining the 
662          * shape of a single fin, where the origin is the leading root edge.  This implementation
663          * calls {@link #getFinPoints()} and adds the necessary points for the fin tab.
664          * The tab coordinates will have a negative y value.
665          * 
666          * @return  List of XY-coordinates.
667          */
668         public Coordinate[] getFinPointsWithTab() {
669                 Coordinate[] points = getFinPoints();
670                 
671                 if (MathUtil.equals(getTabHeight(), 0) ||
672                                 MathUtil.equals(getTabLength(), 0))
673                         return points;
674                 
675                 double x1 = getTabFrontEdge();
676                 double x2 = getTabTrailingEdge();
677                 double y = -getTabHeight();
678                 
679                 int n = points.length;
680                 points = Arrays.copyOf(points, points.length + 4);
681                 points[n] = new Coordinate(x2, 0);
682                 points[n + 1] = new Coordinate(x2, y);
683                 points[n + 2] = new Coordinate(x1, y);
684                 points[n + 3] = new Coordinate(x1, 0);
685                 return points;
686         }
687         
688         
689
690         /**
691          * Get the span of a single fin.  That is, the length from the root to the tip of the fin.
692          * @return  Span of a single fin.
693          */
694         public abstract double getSpan();
695         
696         
697         @Override
698         protected List<RocketComponent> copyFrom(RocketComponent c) {
699                 FinSet src = (FinSet) c;
700                 this.fins = src.fins;
701                 this.finRotation = src.finRotation;
702                 this.rotation = src.rotation;
703                 this.baseRotation = src.baseRotation;
704                 this.cantAngle = src.cantAngle;
705                 this.cantRotation = src.cantRotation;
706                 this.thickness = src.thickness;
707                 this.crossSection = src.crossSection;
708                 this.tabHeight = src.tabHeight;
709                 this.tabLength = src.tabLength;
710                 this.tabRelativePosition = src.tabRelativePosition;
711                 this.tabShift = src.tabShift;
712                 
713                 return super.copyFrom(c);
714         }
715 }