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;
29 CrossSection(String name, double volume) {
34 public double getRelativeVolume() {
39 public String toString() {
44 public enum TabRelativePosition {
45 FRONT("Root chord leading edge"),
46 CENTER("Root chord midpoint"),
47 END("Root chord trailing edge");
49 private final String name;
51 TabRelativePosition(String name) {
56 public String toString() {
64 protected int fins = 3;
67 * Rotation about the x-axis by 2*PI/fins.
69 protected Transformation finRotation = Transformation.rotate_x(2 * Math.PI / fins);
72 * Rotation angle of the first fin. Zero corresponds to the positive y-axis.
74 protected double rotation = 0;
77 * Rotation about the x-axis by angle this.rotation.
79 protected Transformation baseRotation = Transformation.rotate_x(rotation);
85 protected double cantAngle = 0;
88 private Transformation cantRotation = null;
92 * Thickness of the fins.
94 protected double thickness = 0.003;
98 * The cross-section shape of the fins.
100 protected CrossSection crossSection = CrossSection.SQUARE;
104 * Fin tab properties.
106 private double tabHeight = 0;
107 private double tabLength = 0.05;
108 private double tabShift = 0;
109 private TabRelativePosition tabRelativePosition = TabRelativePosition.CENTER;
112 // Cached fin area & CG. Validity of both must be checked using finArea!
113 // Fin area does not include fin tabs, CG does.
114 private double finArea = -1;
115 private double finCGx = -1;
116 private double finCGy = -1;
120 * New FinSet with given number of fins and given base rotation angle.
121 * Sets the component relative position to POSITION_RELATIVE_BOTTOM,
122 * i.e. fins are positioned at the bottom of the parent component.
125 super(RocketComponent.Position.BOTTOM);
131 * Return the number of fins in the set.
132 * @return The number of fins.
134 public int getFinCount() {
139 * Sets the number of fins in the set.
140 * @param n The number of fins, greater of equal to one.
142 public void setFinCount(int n) {
150 finRotation = Transformation.rotate_x(2 * Math.PI / fins);
151 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
154 public Transformation getFinRotationTransformation() {
159 * Gets the base rotation amount of the first fin.
160 * @return The base rotation amount.
162 public double getBaseRotation() {
167 * Sets the base rotation amount of the first fin.
168 * @param r The base rotation amount.
170 public void setBaseRotation(double r) {
171 r = MathUtil.reduce180(r);
172 if (MathUtil.equals(r, rotation))
175 baseRotation = Transformation.rotate_x(rotation);
176 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
179 public Transformation getBaseRotationTransformation() {
185 public double getCantAngle() {
189 public void setCantAngle(double cant) {
190 cant = MathUtil.clamp(cant, -MAX_CANT, MAX_CANT);
191 if (MathUtil.equals(cant, cantAngle))
193 this.cantAngle = cant;
194 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
198 public Transformation getCantRotation() {
199 if (cantRotation == null) {
200 if (MathUtil.equals(cantAngle, 0)) {
201 cantRotation = Transformation.IDENTITY;
203 Transformation t = new Transformation(-length / 2, 0, 0);
204 t = Transformation.rotate_y(cantAngle).applyTransformation(t);
205 t = new Transformation(length / 2, 0, 0).applyTransformation(t);
214 public double getThickness() {
218 public void setThickness(double r) {
221 thickness = Math.max(r, 0);
222 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
226 public CrossSection getCrossSection() {
230 public void setCrossSection(CrossSection cs) {
231 if (crossSection == cs)
234 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
242 public void setRelativePosition(RocketComponent.Position position) {
243 super.setRelativePosition(position);
244 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
249 public void setPositionValue(double value) {
250 super.setPositionValue(value);
251 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
257 public double getTabHeight() {
261 public void setTabHeight(double height) {
262 height = MathUtil.max(height, 0);
263 if (MathUtil.equals(this.tabHeight, height))
265 this.tabHeight = height;
266 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
270 public double getTabLength() {
274 public void setTabLength(double length) {
275 length = MathUtil.max(length, 0);
276 if (MathUtil.equals(this.tabLength, length))
278 this.tabLength = length;
279 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
283 public double getTabShift() {
287 public void setTabShift(double shift) {
288 this.tabShift = shift;
289 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
293 public TabRelativePosition getTabRelativePosition() {
294 return tabRelativePosition;
297 public void setTabRelativePosition(TabRelativePosition position) {
298 if (this.tabRelativePosition == position)
302 double front = getTabFrontEdge();
305 this.tabShift = front;
309 this.tabShift = front + tabLength / 2 - getLength() / 2;
313 this.tabShift = front + tabLength - getLength();
317 throw new IllegalArgumentException("position=" + position);
319 this.tabRelativePosition = position;
321 fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
326 * Return the tab front edge position from the front of the fin.
328 public double getTabFrontEdge() {
329 switch (this.tabRelativePosition) {
334 return getLength() / 2 - tabLength / 2 + tabShift;
337 return getLength() - tabLength + tabShift;
340 throw new IllegalStateException("tabRelativePosition=" + tabRelativePosition);
345 * Return the tab trailing edge position *from the front of the fin*.
347 public double getTabTrailingEdge() {
348 switch (this.tabRelativePosition) {
350 return tabLength + tabShift;
352 return getLength() / 2 + tabLength / 2 + tabShift;
355 return getLength() + tabShift;
358 throw new IllegalStateException("tabRelativePosition=" + tabRelativePosition);
365 /////////// Calculation methods ///////////
368 * Return the area of one side of one fin. This does NOT include the area of
371 * @return the area of one side of one fin.
373 public double getFinArea() {
382 * Return the unweighted CG of a single fin. The X-coordinate is relative to
383 * the root chord trailing edge and the Y-coordinate to the fin root chord.
385 * @return the unweighted CG coordinate of a single fin.
387 public Coordinate getFinCG() {
391 return new Coordinate(finCGx, finCGy, 0);
397 public double getComponentVolume() {
398 return fins * (getFinArea() + tabHeight * tabLength) * thickness *
399 crossSection.getRelativeVolume();
404 public Coordinate getComponentCG() {
408 double mass = getComponentMass(); // safe
411 return baseRotation.transform(
412 new Coordinate(finCGx, finCGy + getBodyRadius(), 0, mass));
414 return new Coordinate(finCGx, 0, 0, mass);
419 private void calculateAreaCG() {
420 Coordinate[] points = this.getFinPoints();
425 for (int i = 0; i < points.length - 1; i++) {
426 final double x0 = points[i].x;
427 final double x1 = points[i + 1].x;
428 final double y0 = points[i].y;
429 final double y1 = points[i + 1].y;
431 double da = (y0 + y1) * (x1 - x0) / 2;
433 if (Math.abs(y0 - y1) < 0.00001) {
434 finCGx += (x0 + x1) / 2 * da;
435 finCGy += y0 / 2 * da;
437 finCGx += (x0 * (2 * y0 + y1) + x1 * (y0 + 2 * y1)) / (3 * (y0 + y1)) * da;
438 finCGy += (y1 + y0 * y0 / (y0 + y1)) / 3 * da;
445 // Add effect of fin tabs to CG
446 double tabArea = tabLength * tabHeight;
447 if (!MathUtil.equals(tabArea, 0)) {
449 double x = (getTabFrontEdge() + getTabTrailingEdge()) / 2;
450 double y = -this.tabHeight / 2;
452 finCGx += x * tabArea;
453 finCGy += y * tabArea;
457 if ((finArea + tabArea) > 0) {
458 finCGx /= (finArea + tabArea);
459 finCGy /= (finArea + tabArea);
461 finCGx = (points[0].x + points[points.length - 1].x) / 2;
468 * Return an approximation of the longitudal unitary inertia of the fin set.
469 * The process is the following:
471 * 1. Approximate the fin with a rectangular fin
473 * 2. The inertia of one fin is taken as the average of the moments of inertia
474 * through its center perpendicular to the plane, and the inertia through
475 * its center parallel to the plane
477 * 3. If there are multiple fins, the inertia is shifted to the center of the fin
478 * set and multiplied by the number of fins.
481 public double getLongitudalUnitInertia() {
482 double area = getFinArea();
483 if (MathUtil.equals(area, 0))
486 // Approximate fin with a rectangular fin
487 // w2 and h2 are squares of the fin width and height
488 double w = getLength();
489 double h = getSpan();
492 if (MathUtil.equals(w * h, 0)) {
500 double inertia = (h2 + 2 * w2) / 24;
505 double radius = getBodyRadius();
507 return fins * (inertia + MathUtil.pow2(Math.sqrt(h2) + radius));
512 * Return an approximation of the rotational unitary inertia of the fin set.
513 * The process is the following:
515 * 1. Approximate the fin with a rectangular fin and calculate the inertia of the
516 * rectangular approximate
518 * 2. If there are multiple fins, shift the inertia center to the fin set center
519 * and multiply with the number of fins.
522 public double getRotationalUnitInertia() {
523 double area = getFinArea();
524 if (MathUtil.equals(area, 0))
527 // Approximate fin with a rectangular fin
528 double w = getLength();
529 double h = getSpan();
531 if (MathUtil.equals(w * h, 0)) {
534 h = Math.sqrt(h * area / w);
540 double radius = getBodyRadius();
542 return fins * (h * h / 12 + MathUtil.pow2(h / 2 + radius));
547 * Adds the fin set's bounds to the collection.
550 public Collection<Coordinate> getComponentBounds() {
551 List<Coordinate> bounds = new ArrayList<Coordinate>();
552 double r = getBodyRadius();
554 for (Coordinate point : getFinPoints()) {
555 addFinBound(bounds, point.x, point.y + r);
563 * Adds the 2d-coordinate bound (x,y) to the collection for both z-components and for
566 private void addFinBound(Collection<Coordinate> set, double x, double y) {
570 c = new Coordinate(x, y, thickness / 2);
571 c = baseRotation.transform(c);
573 for (i = 1; i < fins; i++) {
574 c = finRotation.transform(c);
578 c = new Coordinate(x, y, -thickness / 2);
579 c = baseRotation.transform(c);
581 for (i = 1; i < fins; i++) {
582 c = finRotation.transform(c);
590 public void componentChanged(ComponentChangeEvent e) {
591 if (e.isAerodynamicChange()) {
599 * Return the radius of the BodyComponent the fin set is situated on. Currently
600 * only supports SymmetricComponents and returns the radius at the starting point of the
603 * @return radius of the underlying BodyComponent or 0 if none exists.
605 public double getBodyRadius() {
608 s = this.getParent();
610 if (s instanceof SymmetricComponent) {
611 double x = this.toRelative(new Coordinate(0, 0, 0), s)[0].x;
612 return ((SymmetricComponent) s).getRadius(x);
620 public boolean allowsChildren() {
625 * Allows nothing to be attached to a FinSet.
627 * @return <code>false</code>
630 public boolean isCompatible(Class<? extends RocketComponent> type) {
638 * Return a list of coordinates defining the geometry of a single fin.
639 * The coordinates are the XY-coordinates of points defining the shape of a single fin,
640 * where the origin is the leading root edge. Therefore, the first point must be (0,0,0).
641 * All Z-coordinates must be zero, and the last coordinate must have Y=0.
643 * @return List of XY-coordinates.
645 public abstract Coordinate[] getFinPoints();
649 * Return a list of coordinates defining the geometry of a single fin, including a
650 * possible fin tab. The coordinates are the XY-coordinates of points defining the
651 * shape of a single fin, where the origin is the leading root edge. This implementation
652 * calls {@link #getFinPoints()} and adds the necessary points for the fin tab.
653 * The tab coordinates will have a negative y value.
655 * @return List of XY-coordinates.
657 public Coordinate[] getFinPointsWithTab() {
658 Coordinate[] points = getFinPoints();
660 if (MathUtil.equals(getTabHeight(), 0) ||
661 MathUtil.equals(getTabLength(), 0))
664 double x1 = getTabFrontEdge();
665 double x2 = getTabTrailingEdge();
666 double y = -getTabHeight();
668 int n = points.length;
669 points = Arrays.copyOf(points, points.length + 4);
670 points[n] = new Coordinate(x2, 0);
671 points[n + 1] = new Coordinate(x2, y);
672 points[n + 2] = new Coordinate(x1, y);
673 points[n + 3] = new Coordinate(x1, 0);
680 * Get the span of a single fin. That is, the length from the root to the tip of the fin.
681 * @return Span of a single fin.
683 public abstract double getSpan();
687 protected void copyFrom(RocketComponent c) {
690 FinSet src = (FinSet) c;
691 this.fins = src.fins;
692 this.finRotation = src.finRotation;
693 this.rotation = src.rotation;
694 this.baseRotation = src.baseRotation;
695 this.cantAngle = src.cantAngle;
696 this.cantRotation = src.cantRotation;
697 this.thickness = src.thickness;
698 this.crossSection = src.crossSection;
699 this.tabHeight = src.tabHeight;
700 this.tabLength = src.tabLength;
701 this.tabRelativePosition = src.tabRelativePosition;
702 this.tabShift = src.tabShift;