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.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;
15 public abstract class FinSet extends ExternalComponent {
16 private static final Translator trans = Application.getTranslator();
18 // FIXME: converting triangular fins to freeform fails
21 * Maximum allowed cant of fins.
23 public static final double MAX_CANT = (15.0 * Math.PI / 180);
26 public enum CrossSection {
28 SQUARE(trans.get("FinSet.CrossSection.SQUARE"), 1.00),
30 ROUNDED(trans.get("FinSet.CrossSection.ROUNDED"), 0.99),
32 AIRFOIL(trans.get("FinSet.CrossSection.AIRFOIL"), 0.85);
34 private final String name;
35 private final double volume;
37 CrossSection(String name, double volume) {
42 public double getRelativeVolume() {
47 public String toString() {
52 public enum TabRelativePosition {
53 //// Root chord leading edge
54 FRONT(trans.get("FinSet.TabRelativePosition.FRONT")),
55 //// Root chord midpoint
56 CENTER(trans.get("FinSet.TabRelativePosition.CENTER")),
57 //// Root chord trailing edge
58 END(trans.get("FinSet.TabRelativePosition.END"));
60 private final String name;
62 TabRelativePosition(String name) {
67 public String toString() {
75 protected int fins = 3;
78 * Rotation about the x-axis by 2*PI/fins.
80 protected Transformation finRotation = Transformation.rotate_x(2 * Math.PI / fins);
83 * Rotation angle of the first fin. Zero corresponds to the positive y-axis.
85 protected double rotation = 0;
88 * Rotation about the x-axis by angle this.rotation.
90 protected Transformation baseRotation = Transformation.rotate_x(rotation);
96 protected double cantAngle = 0;
99 private Transformation cantRotation = null;
103 * Thickness of the fins.
105 protected double thickness = 0.003;
109 * The cross-section shape of the fins.
111 protected CrossSection crossSection = CrossSection.SQUARE;
115 * Fin tab properties.
117 private double tabHeight = 0;
118 private double tabLength = 0.05;
119 private double tabShift = 0;
120 private TabRelativePosition tabRelativePosition = TabRelativePosition.CENTER;
123 // Cached fin area & CG. Validity of both must be checked using finArea!
124 // Fin area does not include fin tabs, CG does.
125 private double finArea = -1;
126 private double finCGx = -1;
127 private double finCGy = -1;
131 * New FinSet with given number of fins and given base rotation angle.
132 * Sets the component relative position to POSITION_RELATIVE_BOTTOM,
133 * i.e. fins are positioned at the bottom of the parent component.
136 super(RocketComponent.Position.BOTTOM);
142 * Return the number of fins in the set.
143 * @return The number of fins.
145 public int getFinCount() {
150 * Sets the number of fins in the set.
151 * @param n The number of fins, greater of equal to one.
153 public void setFinCount(int n) {
161 finRotation = Transformation.rotate_x(2 * Math.PI / fins);
162 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
165 public Transformation getFinRotationTransformation() {
170 * Gets the base rotation amount of the first fin.
171 * @return The base rotation amount.
173 public double getBaseRotation() {
178 * Sets the base rotation amount of the first fin.
179 * @param r The base rotation amount.
181 public void setBaseRotation(double r) {
182 r = MathUtil.reduce180(r);
183 if (MathUtil.equals(r, rotation))
186 baseRotation = Transformation.rotate_x(rotation);
187 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
190 public Transformation getBaseRotationTransformation() {
196 public double getCantAngle() {
200 public void setCantAngle(double cant) {
201 cant = MathUtil.clamp(cant, -MAX_CANT, MAX_CANT);
202 if (MathUtil.equals(cant, cantAngle))
204 this.cantAngle = cant;
205 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
209 public Transformation getCantRotation() {
210 if (cantRotation == null) {
211 if (MathUtil.equals(cantAngle, 0)) {
212 cantRotation = Transformation.IDENTITY;
214 Transformation t = new Transformation(-length / 2, 0, 0);
215 t = Transformation.rotate_y(cantAngle).applyTransformation(t);
216 t = new Transformation(length / 2, 0, 0).applyTransformation(t);
225 public double getThickness() {
229 public void setThickness(double r) {
232 thickness = Math.max(r, 0);
233 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
237 public CrossSection getCrossSection() {
241 public void setCrossSection(CrossSection cs) {
242 if (crossSection == cs)
245 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
253 public void setRelativePosition(RocketComponent.Position position) {
254 super.setRelativePosition(position);
255 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
260 public void setPositionValue(double value) {
261 super.setPositionValue(value);
262 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
268 public double getTabHeight() {
272 public void setTabHeight(double height) {
273 height = MathUtil.max(height, 0);
274 if (MathUtil.equals(this.tabHeight, height))
276 this.tabHeight = height;
277 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
281 public double getTabLength() {
285 public void setTabLength(double length) {
286 length = MathUtil.max(length, 0);
287 if (MathUtil.equals(this.tabLength, length))
289 this.tabLength = length;
290 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
294 public double getTabShift() {
298 public void setTabShift(double shift) {
299 this.tabShift = shift;
300 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
304 public TabRelativePosition getTabRelativePosition() {
305 return tabRelativePosition;
308 public void setTabRelativePosition(TabRelativePosition position) {
309 if (this.tabRelativePosition == position)
313 double front = getTabFrontEdge();
316 this.tabShift = front;
320 this.tabShift = front + tabLength / 2 - getLength() / 2;
324 this.tabShift = front + tabLength - getLength();
328 throw new IllegalArgumentException("position=" + position);
330 this.tabRelativePosition = position;
332 fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
337 * Return the tab front edge position from the front of the fin.
339 public double getTabFrontEdge() {
340 switch (this.tabRelativePosition) {
345 return getLength() / 2 - tabLength / 2 + tabShift;
348 return getLength() - tabLength + tabShift;
351 throw new IllegalStateException("tabRelativePosition=" + tabRelativePosition);
356 * Return the tab trailing edge position *from the front of the fin*.
358 public double getTabTrailingEdge() {
359 switch (this.tabRelativePosition) {
361 return tabLength + tabShift;
363 return getLength() / 2 + tabLength / 2 + tabShift;
366 return getLength() + tabShift;
369 throw new IllegalStateException("tabRelativePosition=" + tabRelativePosition);
376 /////////// Calculation methods ///////////
379 * Return the area of one side of one fin. This does NOT include the area of
382 * @return the area of one side of one fin.
384 public double getFinArea() {
393 * Return the unweighted CG of a single fin. The X-coordinate is relative to
394 * the root chord trailing edge and the Y-coordinate to the fin root chord.
396 * @return the unweighted CG coordinate of a single fin.
398 public Coordinate getFinCG() {
402 return new Coordinate(finCGx, finCGy, 0);
408 public double getComponentVolume() {
409 return fins * (getFinArea() + tabHeight * tabLength) * thickness *
410 crossSection.getRelativeVolume();
415 public Coordinate getComponentCG() {
419 double mass = getComponentMass(); // safe
422 return baseRotation.transform(
423 new Coordinate(finCGx, finCGy + getBodyRadius(), 0, mass));
425 return new Coordinate(finCGx, 0, 0, mass);
430 private void calculateAreaCG() {
431 Coordinate[] points = this.getFinPoints();
436 for (int i = 0; i < points.length - 1; i++) {
437 final double x0 = points[i].x;
438 final double x1 = points[i + 1].x;
439 final double y0 = points[i].y;
440 final double y1 = points[i + 1].y;
442 double da = (y0 + y1) * (x1 - x0) / 2;
444 if (Math.abs(y0 - y1) < 0.00001) {
445 finCGx += (x0 + x1) / 2 * da;
446 finCGy += y0 / 2 * da;
448 finCGx += (x0 * (2 * y0 + y1) + x1 * (y0 + 2 * y1)) / (3 * (y0 + y1)) * da;
449 finCGy += (y1 + y0 * y0 / (y0 + y1)) / 3 * da;
456 // Add effect of fin tabs to CG
457 double tabArea = tabLength * tabHeight;
458 if (!MathUtil.equals(tabArea, 0)) {
460 double x = (getTabFrontEdge() + getTabTrailingEdge()) / 2;
461 double y = -this.tabHeight / 2;
463 finCGx += x * tabArea;
464 finCGy += y * tabArea;
468 if ((finArea + tabArea) > 0) {
469 finCGx /= (finArea + tabArea);
470 finCGy /= (finArea + tabArea);
472 finCGx = (points[0].x + points[points.length - 1].x) / 2;
479 * Return an approximation of the longitudinal unitary inertia of the fin set.
480 * The process is the following:
482 * 1. Approximate the fin with a rectangular fin
484 * 2. The inertia of one fin is taken as the average of the moments of inertia
485 * through its center perpendicular to the plane, and the inertia through
486 * its center parallel to the plane
488 * 3. If there are multiple fins, the inertia is shifted to the center of the fin
489 * set and multiplied by the number of fins.
492 public double getLongitudinalUnitInertia() {
493 double area = getFinArea();
494 if (MathUtil.equals(area, 0))
497 // Approximate fin with a rectangular fin
498 // w2 and h2 are squares of the fin width and height
499 double w = getLength();
500 double h = getSpan();
503 if (MathUtil.equals(w * h, 0)) {
511 double inertia = (h2 + 2 * w2) / 24;
516 double radius = getBodyRadius();
518 return fins * (inertia + MathUtil.pow2(Math.sqrt(h2) + radius));
523 * Return an approximation of the rotational unitary inertia of the fin set.
524 * The process is the following:
526 * 1. Approximate the fin with a rectangular fin and calculate the inertia of the
527 * rectangular approximate
529 * 2. If there are multiple fins, shift the inertia center to the fin set center
530 * and multiply with the number of fins.
533 public double getRotationalUnitInertia() {
534 double area = getFinArea();
535 if (MathUtil.equals(area, 0))
538 // Approximate fin with a rectangular fin
539 double w = getLength();
540 double h = getSpan();
542 if (MathUtil.equals(w * h, 0)) {
545 h = Math.sqrt(h * area / w);
551 double radius = getBodyRadius();
553 return fins * (h * h / 12 + MathUtil.pow2(h / 2 + radius));
558 * Adds the fin set's bounds to the collection.
561 public Collection<Coordinate> getComponentBounds() {
562 List<Coordinate> bounds = new ArrayList<Coordinate>();
563 double r = getBodyRadius();
565 for (Coordinate point : getFinPoints()) {
566 addFinBound(bounds, point.x, point.y + r);
574 * Adds the 2d-coordinate bound (x,y) to the collection for both z-components and for
577 private void addFinBound(Collection<Coordinate> set, double x, double y) {
581 c = new Coordinate(x, y, thickness / 2);
582 c = baseRotation.transform(c);
584 for (i = 1; i < fins; i++) {
585 c = finRotation.transform(c);
589 c = new Coordinate(x, y, -thickness / 2);
590 c = baseRotation.transform(c);
592 for (i = 1; i < fins; i++) {
593 c = finRotation.transform(c);
601 public void componentChanged(ComponentChangeEvent e) {
602 if (e.isAerodynamicChange()) {
610 * Return the radius of the BodyComponent the fin set is situated on. Currently
611 * only supports SymmetricComponents and returns the radius at the starting point of the
614 * @return radius of the underlying BodyComponent or 0 if none exists.
616 public double getBodyRadius() {
619 s = this.getParent();
621 if (s instanceof SymmetricComponent) {
622 double x = this.toRelative(new Coordinate(0, 0, 0), s)[0].x;
623 return ((SymmetricComponent) s).getRadius(x);
631 public boolean allowsChildren() {
636 * Allows nothing to be attached to a FinSet.
638 * @return <code>false</code>
641 public boolean isCompatible(Class<? extends RocketComponent> type) {
649 * Return a list of coordinates defining the geometry of a single fin.
650 * The coordinates are the XY-coordinates of points defining the shape of a single fin,
651 * where the origin is the leading root edge. Therefore, the first point must be (0,0,0).
652 * All Z-coordinates must be zero, and the last coordinate must have Y=0.
654 * @return List of XY-coordinates.
656 public abstract Coordinate[] getFinPoints();
660 * Return a list of coordinates defining the geometry of a single fin, including a
661 * possible fin tab. The coordinates are the XY-coordinates of points defining the
662 * shape of a single fin, where the origin is the leading root edge. This implementation
663 * calls {@link #getFinPoints()} and adds the necessary points for the fin tab.
664 * The tab coordinates will have a negative y value.
666 * @return List of XY-coordinates.
668 public Coordinate[] getFinPointsWithTab() {
669 Coordinate[] points = getFinPoints();
671 if (MathUtil.equals(getTabHeight(), 0) ||
672 MathUtil.equals(getTabLength(), 0))
675 double x1 = getTabFrontEdge();
676 double x2 = getTabTrailingEdge();
677 double y = -getTabHeight();
679 int n = points.length;
680 points = Arrays.copyOf(points, points.length + 4);
681 points[n] = new Coordinate(x2, 0);
682 points[n + 1] = new Coordinate(x2, y);
683 points[n + 2] = new Coordinate(x1, y);
684 points[n + 3] = new Coordinate(x1, 0);
691 * Get the span of a single fin. That is, the length from the root to the tip of the fin.
692 * @return Span of a single fin.
694 public abstract double getSpan();
698 protected List<RocketComponent> copyFrom(RocketComponent c) {
699 FinSet src = (FinSet) c;
700 this.fins = src.fins;
701 this.finRotation = src.finRotation;
702 this.rotation = src.rotation;
703 this.baseRotation = src.baseRotation;
704 this.cantAngle = src.cantAngle;
705 this.cantRotation = src.cantRotation;
706 this.thickness = src.thickness;
707 this.crossSection = src.crossSection;
708 this.tabHeight = src.tabHeight;
709 this.tabLength = src.tabLength;
710 this.tabRelativePosition = src.tabRelativePosition;
711 this.tabShift = src.tabShift;
713 return super.copyFrom(c);