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