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