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