1 package net.sf.openrocket.rocketcomponent;
3 import java.util.ArrayList;
4 import java.util.Arrays;
5 import java.util.Collection;
8 import net.sf.openrocket.util.Coordinate;
9 import net.sf.openrocket.util.MathUtil;
10 import net.sf.openrocket.util.Transformation;
13 public abstract class FinSet extends ExternalComponent {
16 * Maximum allowed cant of fins.
18 public static final double MAX_CANT = (15.0 * Math.PI / 180);
21 public enum CrossSection {
22 SQUARE("Square", 1.00),
23 ROUNDED("Rounded", 0.99),
24 AIRFOIL("Airfoil", 0.85);
26 private final String name;
27 private final double volume;
28 CrossSection(String name, double volume) {
33 public double getRelativeVolume() {
37 public String toString() {
42 public enum TabRelativePosition {
43 FRONT("Root chord leading edge"),
44 CENTER("Root chord midpoint"),
45 END("Root chord trailing edge");
47 private final String name;
48 TabRelativePosition(String name) {
53 public String toString() {
61 protected int fins = 3;
64 * Rotation about the x-axis by 2*PI/fins.
66 protected Transformation finRotation = Transformation.rotate_x(2*Math.PI/fins);
69 * Rotation angle of the first fin. Zero corresponds to the positive y-axis.
71 protected double rotation = 0;
74 * Rotation about the x-axis by angle this.rotation.
76 protected Transformation baseRotation = Transformation.rotate_x(rotation);
82 protected double cantAngle = 0;
85 private Transformation cantRotation = null;
89 * Thickness of the fins.
91 protected double thickness = 0.003;
95 * The cross-section shape of the fins.
97 protected CrossSection crossSection = CrossSection.SQUARE;
101 * Fin tab properties.
103 private double tabHeight = 0;
104 private double tabLength = 0.05;
105 private double tabShift = 0;
106 private TabRelativePosition tabRelativePosition = TabRelativePosition.CENTER;
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;
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.
122 super(RocketComponent.Position.BOTTOM);
128 * Return the number of fins in the set.
129 * @return The number of fins.
131 public int getFinCount() {
136 * Sets the number of fins in the set.
137 * @param n The number of fins, greater of equal to one.
139 public void setFinCount(int n) {
147 finRotation = Transformation.rotate_x(2*Math.PI/fins);
148 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
151 public Transformation getFinRotationTransformation() {
156 * Gets the base rotation amount of the first fin.
157 * @return The base rotation amount.
159 public double getBaseRotation() {
164 * Sets the base rotation amount of the first fin.
165 * @param r The base rotation amount.
167 public void setBaseRotation(double r) {
168 r = MathUtil.reduce180(r);
169 if (MathUtil.equals(r, rotation))
172 baseRotation = Transformation.rotate_x(rotation);
173 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
176 public Transformation getBaseRotationTransformation() {
182 public double getCantAngle() {
186 public void setCantAngle(double cant) {
187 cant = MathUtil.clamp(cant, -MAX_CANT, MAX_CANT);
188 if (MathUtil.equals(cant, cantAngle))
190 this.cantAngle = cant;
191 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
195 public Transformation getCantRotation() {
196 if (cantRotation == null) {
197 if (MathUtil.equals(cantAngle,0)) {
198 cantRotation = Transformation.IDENTITY;
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);
211 public double getThickness() {
215 public void setThickness(double r) {
218 thickness = Math.max(r,0);
219 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
223 public CrossSection getCrossSection() {
227 public void setCrossSection(CrossSection cs) {
228 if (crossSection == cs)
231 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
239 public void setRelativePosition(RocketComponent.Position position) {
240 super.setRelativePosition(position);
241 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
246 public void setPositionValue(double value) {
247 super.setPositionValue(value);
248 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
254 public double getTabHeight() {
258 public void setTabHeight(double height) {
259 height = MathUtil.max(height, 0);
260 if (MathUtil.equals(this.tabHeight, height))
262 this.tabHeight = height;
263 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
267 public double getTabLength() {
271 public void setTabLength(double length) {
272 length = MathUtil.max(length, 0);
273 if (MathUtil.equals(this.tabLength, length))
275 this.tabLength = length;
276 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
280 public double getTabShift() {
284 public void setTabShift(double shift) {
285 this.tabShift = shift;
286 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
290 public TabRelativePosition getTabRelativePosition() {
291 return tabRelativePosition;
294 public void setTabRelativePosition(TabRelativePosition position) {
295 if (this.tabRelativePosition == position)
299 double front = getTabFrontEdge();
302 this.tabShift = front;
306 this.tabShift = front + tabLength/2 - getLength()/2;
310 this.tabShift = front + tabLength - getLength();
314 throw new IllegalArgumentException("position="+position);
316 this.tabRelativePosition = position;
318 fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
323 * Return the tab front edge position from the front of the fin.
325 public double getTabFrontEdge() {
326 switch (this.tabRelativePosition) {
331 return getLength()/2 - tabLength/2 + tabShift;
334 return getLength() - tabLength + tabShift;
337 throw new IllegalStateException("tabRelativePosition="+tabRelativePosition);
342 * Return the tab trailing edge position *from the front of the fin*.
344 public double getTabTrailingEdge() {
345 switch (this.tabRelativePosition) {
347 return tabLength + tabShift;
349 return getLength()/2 + tabLength/2 + tabShift;
352 return getLength() + tabShift;
355 throw new IllegalStateException("tabRelativePosition="+tabRelativePosition);
362 /////////// Calculation methods ///////////
365 * Return the area of one side of one fin. This does NOT include the area of
368 * @return the area of one side of one fin.
370 public double getFinArea() {
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.
382 * @return the unweighted CG coordinate of a single fin.
384 public Coordinate getFinCG() {
388 return new Coordinate(finCGx,finCGy,0);
394 public double getComponentVolume() {
395 return fins * (getFinArea() + tabHeight*tabLength) * thickness *
396 crossSection.getRelativeVolume();
401 public Coordinate getComponentCG() {
405 double mass = getComponentMass(); // safe
408 return baseRotation.transform(
409 new Coordinate(finCGx,finCGy + getBodyRadius(), 0, mass));
411 return new Coordinate(finCGx, 0, 0, mass);
416 private void calculateAreaCG() {
417 Coordinate[] points = this.getFinPoints();
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;
428 double da = (y0+y1)*(x1-x0) / 2;
430 if (Math.abs(y0-y1) < 0.00001) {
431 finCGx += (x0+x1)/2 * da;
434 finCGx += (x0*(2*y0 + y1) + x1*(y0 + 2*y1)) / (3*(y0 + y1)) * da;
435 finCGy += (y1 + y0*y0/(y0 + y1))/3 * da;
442 // Add effect of fin tabs to CG
443 double tabArea = tabLength * tabHeight;
444 if (!MathUtil.equals(tabArea, 0)) {
446 double x = (getTabFrontEdge() + getTabTrailingEdge())/2;
447 double y = -this.tabHeight/2;
454 if ((finArea + tabArea) > 0) {
455 finCGx /= (finArea + tabArea);
456 finCGy /= (finArea + tabArea);
458 finCGx = (points[0].x + points[points.length-1].x)/2;
465 * Return an approximation of the longitudal unitary inertia of the fin set.
466 * The process is the following:
468 * 1. Approximate the fin with a rectangular fin
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
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.
478 public double getLongitudalUnitInertia() {
479 double area = getFinArea();
480 if (MathUtil.equals(area, 0))
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();
489 if (MathUtil.equals(w*h,0)) {
497 double inertia = (h2 + 2*w2)/24;
502 double radius = getBodyRadius();
504 return fins * (inertia + MathUtil.pow2(Math.sqrt(h2) + radius));
509 * Return an approximation of the rotational unitary inertia of the fin set.
510 * The process is the following:
512 * 1. Approximate the fin with a rectangular fin and calculate the inertia of the
513 * rectangular approximate
515 * 2. If there are multiple fins, shift the inertia center to the fin set center
516 * and multiply with the number of fins.
519 public double getRotationalUnitInertia() {
520 double area = getFinArea();
521 if (MathUtil.equals(area, 0))
524 // Approximate fin with a rectangular fin
525 double w = getLength();
526 double h = getSpan();
528 if (MathUtil.equals(w*h,0)) {
531 h = Math.sqrt(h*area/w);
537 double radius = getBodyRadius();
539 return fins * (h*h/12 + MathUtil.pow2(h/2 + radius));
544 * Adds the fin set's bounds to the collection.
547 public Collection<Coordinate> getComponentBounds() {
548 List<Coordinate> bounds = new ArrayList<Coordinate>();
549 double r = getBodyRadius();
551 for (Coordinate point: getFinPoints()) {
552 addFinBound(bounds, point.x, point.y + r);
560 * Adds the 2d-coordinate bound (x,y) to the collection for both z-components and for
563 private void addFinBound(Collection<Coordinate> set, double x, double y) {
567 c = new Coordinate(x,y,thickness/2);
568 c = baseRotation.transform(c);
570 for (i=1; i<fins; i++) {
571 c = finRotation.transform(c);
575 c = new Coordinate(x,y,-thickness/2);
576 c = baseRotation.transform(c);
578 for (i=1; i<fins; i++) {
579 c = finRotation.transform(c);
587 public void componentChanged(ComponentChangeEvent e) {
588 if (e.isAerodynamicChange()) {
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
600 * @return radius of the underlying BodyComponent or 0 if none exists.
602 public double getBodyRadius() {
605 s = this.getParent();
607 if (s instanceof SymmetricComponent) {
608 double x = this.toRelative(new Coordinate(0,0,0), s)[0].x;
609 return ((SymmetricComponent)s).getRadius(x);
617 * Allows nothing to be attached to a FinSet.
619 * @return <code>false</code>
622 public boolean isCompatible(Class<? extends RocketComponent> type) {
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.
635 * @return List of XY-coordinates.
637 public abstract Coordinate[] getFinPoints();
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.
647 * @return List of XY-coordinates.
649 public Coordinate[] getFinPointsWithTab() {
650 Coordinate[] points = getFinPoints();
652 if (MathUtil.equals(getTabHeight(), 0) ||
653 MathUtil.equals(getTabLength(), 0))
656 double x1 = getTabFrontEdge();
657 double x2 = getTabTrailingEdge();
658 double y = -getTabHeight();
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);
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.
675 public abstract double getSpan();
679 protected void copyFrom(RocketComponent c) {
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;