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.ArrayUtils;
11 import net.sf.openrocket.util.Coordinate;
12 import net.sf.openrocket.util.MathUtil;
13 import net.sf.openrocket.util.Transformation;
16 public abstract class FinSet extends ExternalComponent {
17 private static final Translator trans = Application.getTranslator();
20 * Maximum allowed cant of fins.
22 public static final double MAX_CANT = (15.0 * Math.PI / 180);
25 public enum CrossSection {
27 SQUARE(trans.get("FinSet.CrossSection.SQUARE"), 1.00),
29 ROUNDED(trans.get("FinSet.CrossSection.ROUNDED"), 0.99),
31 AIRFOIL(trans.get("FinSet.CrossSection.AIRFOIL"), 0.85);
33 private final String name;
34 private final double volume;
36 CrossSection(String name, double volume) {
41 public double getRelativeVolume() {
46 public String toString() {
51 public enum TabRelativePosition {
52 //// Root chord leading edge
53 FRONT(trans.get("FinSet.TabRelativePosition.FRONT")),
54 //// Root chord midpoint
55 CENTER(trans.get("FinSet.TabRelativePosition.CENTER")),
56 //// Root chord trailing edge
57 END(trans.get("FinSet.TabRelativePosition.END"));
59 private final String name;
61 TabRelativePosition(String name) {
66 public String toString() {
74 protected int fins = 3;
77 * Rotation about the x-axis by 2*PI/fins.
79 protected Transformation finRotation = Transformation.rotate_x(2 * Math.PI / fins);
82 * Rotation angle of the first fin. Zero corresponds to the positive y-axis.
84 protected double rotation = 0;
87 * Rotation about the x-axis by angle this.rotation.
89 protected Transformation baseRotation = Transformation.rotate_x(rotation);
95 protected double cantAngle = 0;
98 private Transformation cantRotation = null;
102 * Thickness of the fins.
104 protected double thickness = 0.003;
108 * The cross-section shape of the fins.
110 protected CrossSection crossSection = CrossSection.SQUARE;
114 * Fin tab properties.
116 private double tabHeight = 0;
117 private double tabLength = 0.05;
118 private double tabShift = 0;
119 private TabRelativePosition tabRelativePosition = TabRelativePosition.CENTER;
122 // Cached fin area & CG. Validity of both must be checked using finArea!
123 // Fin area does not include fin tabs, CG does.
124 private double finArea = -1;
125 private double finCGx = -1;
126 private double finCGy = -1;
130 * New FinSet with given number of fins and given base rotation angle.
131 * Sets the component relative position to POSITION_RELATIVE_BOTTOM,
132 * i.e. fins are positioned at the bottom of the parent component.
135 super(RocketComponent.Position.BOTTOM);
141 * Return the number of fins in the set.
142 * @return The number of fins.
144 public int getFinCount() {
149 * Sets the number of fins in the set.
150 * @param n The number of fins, greater of equal to one.
152 public void setFinCount(int n) {
160 finRotation = Transformation.rotate_x(2 * Math.PI / fins);
161 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
164 public Transformation getFinRotationTransformation() {
169 * Gets the base rotation amount of the first fin.
170 * @return The base rotation amount.
172 public double getBaseRotation() {
177 * Sets the base rotation amount of the first fin.
178 * @param r The base rotation amount.
180 public void setBaseRotation(double r) {
181 r = MathUtil.reduce180(r);
182 if (MathUtil.equals(r, rotation))
185 baseRotation = Transformation.rotate_x(rotation);
186 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
189 public Transformation getBaseRotationTransformation() {
195 public double getCantAngle() {
199 public void setCantAngle(double cant) {
200 cant = MathUtil.clamp(cant, -MAX_CANT, MAX_CANT);
201 if (MathUtil.equals(cant, cantAngle))
203 this.cantAngle = cant;
204 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
208 public Transformation getCantRotation() {
209 if (cantRotation == null) {
210 if (MathUtil.equals(cantAngle, 0)) {
211 cantRotation = Transformation.IDENTITY;
213 Transformation t = new Transformation(-length / 2, 0, 0);
214 t = Transformation.rotate_y(cantAngle).applyTransformation(t);
215 t = new Transformation(length / 2, 0, 0).applyTransformation(t);
224 public double getThickness() {
228 public void setThickness(double r) {
231 thickness = Math.max(r, 0);
232 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
236 public CrossSection getCrossSection() {
240 public void setCrossSection(CrossSection cs) {
241 if (crossSection == cs)
244 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
252 public void setRelativePosition(RocketComponent.Position position) {
253 super.setRelativePosition(position);
254 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
259 public void setPositionValue(double value) {
260 super.setPositionValue(value);
261 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
267 public double getTabHeight() {
271 public void setTabHeight(double height) {
272 height = MathUtil.max(height, 0);
273 if (MathUtil.equals(this.tabHeight, height))
275 this.tabHeight = height;
276 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
280 public double getTabLength() {
284 public void setTabLength(double length) {
285 length = MathUtil.max(length, 0);
286 if (MathUtil.equals(this.tabLength, length))
288 this.tabLength = length;
289 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
293 public double getTabShift() {
297 public void setTabShift(double shift) {
298 this.tabShift = shift;
299 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
303 public TabRelativePosition getTabRelativePosition() {
304 return tabRelativePosition;
307 public void setTabRelativePosition(TabRelativePosition position) {
308 if (this.tabRelativePosition == position)
312 double front = getTabFrontEdge();
315 this.tabShift = front;
319 this.tabShift = front + tabLength / 2 - getLength() / 2;
323 this.tabShift = front + tabLength - getLength();
327 throw new IllegalArgumentException("position=" + position);
329 this.tabRelativePosition = position;
331 fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
336 * Return the tab front edge position from the front of the fin.
338 public double getTabFrontEdge() {
339 switch (this.tabRelativePosition) {
344 return getLength() / 2 - tabLength / 2 + tabShift;
347 return getLength() - tabLength + tabShift;
350 throw new IllegalStateException("tabRelativePosition=" + tabRelativePosition);
355 * Return the tab trailing edge position *from the front of the fin*.
357 public double getTabTrailingEdge() {
358 switch (this.tabRelativePosition) {
360 return tabLength + tabShift;
362 return getLength() / 2 + tabLength / 2 + tabShift;
365 return getLength() + tabShift;
368 throw new IllegalStateException("tabRelativePosition=" + tabRelativePosition);
375 /////////// Calculation methods ///////////
378 * Return the area of one side of one fin. This does NOT include the area of
381 * @return the area of one side of one fin.
383 public double getFinArea() {
392 * Return the unweighted CG of a single fin. The X-coordinate is relative to
393 * the root chord trailing edge and the Y-coordinate to the fin root chord.
395 * @return the unweighted CG coordinate of a single fin.
397 public Coordinate getFinCG() {
401 return new Coordinate(finCGx, finCGy, 0);
407 public double getComponentVolume() {
408 return fins * (getFinArea() + tabHeight * tabLength) * thickness *
409 crossSection.getRelativeVolume();
414 public Coordinate getComponentCG() {
418 double mass = getComponentMass(); // safe
421 return baseRotation.transform(
422 new Coordinate(finCGx, finCGy + getBodyRadius(), 0, mass));
424 return new Coordinate(finCGx, 0, 0, mass);
429 private void calculateAreaCG() {
430 Coordinate[] points = this.getFinPoints();
435 for (int i = 0; i < points.length - 1; i++) {
436 final double x0 = points[i].x;
437 final double x1 = points[i + 1].x;
438 final double y0 = points[i].y;
439 final double y1 = points[i + 1].y;
441 double da = (y0 + y1) * (x1 - x0) / 2;
443 if (Math.abs(y0 - y1) < 0.00001) {
444 finCGx += (x0 + x1) / 2 * da;
445 finCGy += y0 / 2 * da;
447 finCGx += (x0 * (2 * y0 + y1) + x1 * (y0 + 2 * y1)) / (3 * (y0 + y1)) * da;
448 finCGy += (y1 + y0 * y0 / (y0 + y1)) / 3 * da;
455 // Add effect of fin tabs to CG
456 double tabArea = tabLength * tabHeight;
457 if (!MathUtil.equals(tabArea, 0)) {
459 double x = (getTabFrontEdge() + getTabTrailingEdge()) / 2;
460 double y = -this.tabHeight / 2;
462 finCGx += x * tabArea;
463 finCGy += y * tabArea;
467 if ((finArea + tabArea) > 0) {
468 finCGx /= (finArea + tabArea);
469 finCGy /= (finArea + tabArea);
471 finCGx = (points[0].x + points[points.length - 1].x) / 2;
478 * Return an approximation of the longitudinal unitary inertia of the fin set.
479 * The process is the following:
481 * 1. Approximate the fin with a rectangular fin
483 * 2. The inertia of one fin is taken as the average of the moments of inertia
484 * through its center perpendicular to the plane, and the inertia through
485 * its center parallel to the plane
487 * 3. If there are multiple fins, the inertia is shifted to the center of the fin
488 * set and multiplied by the number of fins.
491 public double getLongitudinalUnitInertia() {
492 double area = getFinArea();
493 if (MathUtil.equals(area, 0))
496 // Approximate fin with a rectangular fin
497 // w2 and h2 are squares of the fin width and height
498 double w = getLength();
499 double h = getSpan();
502 if (MathUtil.equals(w * h, 0)) {
510 double inertia = (h2 + 2 * w2) / 24;
515 double radius = getBodyRadius();
517 return fins * (inertia + MathUtil.pow2(MathUtil.safeSqrt(h2) + radius));
522 * Return an approximation of the rotational unitary inertia of the fin set.
523 * The process is the following:
525 * 1. Approximate the fin with a rectangular fin and calculate the inertia of the
526 * rectangular approximate
528 * 2. If there are multiple fins, shift the inertia center to the fin set center
529 * and multiply with the number of fins.
532 public double getRotationalUnitInertia() {
533 double area = getFinArea();
534 if (MathUtil.equals(area, 0))
537 // Approximate fin with a rectangular fin
538 double w = getLength();
539 double h = getSpan();
541 if (MathUtil.equals(w * h, 0)) {
542 h = MathUtil.safeSqrt(area);
544 h = MathUtil.safeSqrt(h * area / w);
550 double radius = getBodyRadius();
552 return fins * (h * h / 12 + MathUtil.pow2(h / 2 + radius));
557 * Adds the fin set's bounds to the collection.
560 public Collection<Coordinate> getComponentBounds() {
561 List<Coordinate> bounds = new ArrayList<Coordinate>();
562 double r = getBodyRadius();
564 for (Coordinate point : getFinPoints()) {
565 addFinBound(bounds, point.x, point.y + r);
573 * Adds the 2d-coordinate bound (x,y) to the collection for both z-components and for
576 private void addFinBound(Collection<Coordinate> set, double x, double y) {
580 c = new Coordinate(x, y, thickness / 2);
581 c = baseRotation.transform(c);
583 for (i = 1; i < fins; i++) {
584 c = finRotation.transform(c);
588 c = new Coordinate(x, y, -thickness / 2);
589 c = baseRotation.transform(c);
591 for (i = 1; i < fins; i++) {
592 c = finRotation.transform(c);
600 public void componentChanged(ComponentChangeEvent e) {
601 if (e.isAerodynamicChange()) {
609 * Return the radius of the BodyComponent the fin set is situated on. Currently
610 * only supports SymmetricComponents and returns the radius at the starting point of the
613 * @return radius of the underlying BodyComponent or 0 if none exists.
615 public double getBodyRadius() {
618 s = this.getParent();
620 if (s instanceof SymmetricComponent) {
621 double x = this.toRelative(new Coordinate(0, 0, 0), s)[0].x;
622 return ((SymmetricComponent) s).getRadius(x);
630 public boolean allowsChildren() {
635 * Allows nothing to be attached to a FinSet.
637 * @return <code>false</code>
640 public boolean isCompatible(Class<? extends RocketComponent> type) {
648 * Return a list of coordinates defining the geometry of a single fin.
649 * The coordinates are the XY-coordinates of points defining the shape of a single fin,
650 * where the origin is the leading root edge. Therefore, the first point must be (0,0,0).
651 * All Z-coordinates must be zero, and the last coordinate must have Y=0.
653 * @return List of XY-coordinates.
655 public abstract Coordinate[] getFinPoints();
659 * Return a list of coordinates defining the geometry of a single fin, including a
660 * possible fin tab. The coordinates are the XY-coordinates of points defining the
661 * shape of a single fin, where the origin is the leading root edge. This implementation
662 * calls {@link #getFinPoints()} and adds the necessary points for the fin tab.
663 * The tab coordinates will have a negative y value.
665 * @return List of XY-coordinates.
667 public Coordinate[] getFinPointsWithTab() {
668 Coordinate[] points = getFinPoints();
670 if (MathUtil.equals(getTabHeight(), 0) ||
671 MathUtil.equals(getTabLength(), 0))
674 double x1 = getTabFrontEdge();
675 double x2 = getTabTrailingEdge();
676 double y = -getTabHeight();
678 int n = points.length;
679 points = ArrayUtils.copyOf(points, points.length + 4);
680 points[n] = new Coordinate(x2, 0);
681 points[n + 1] = new Coordinate(x2, y);
682 points[n + 2] = new Coordinate(x1, y);
683 points[n + 3] = new Coordinate(x1, 0);
690 * Get the span of a single fin. That is, the length from the root to the tip of the fin.
691 * @return Span of a single fin.
693 public abstract double getSpan();
697 protected List<RocketComponent> copyFrom(RocketComponent c) {
698 FinSet src = (FinSet) c;
699 this.fins = src.fins;
700 this.finRotation = src.finRotation;
701 this.rotation = src.rotation;
702 this.baseRotation = src.baseRotation;
703 this.cantAngle = src.cantAngle;
704 this.cantRotation = src.cantRotation;
705 this.thickness = src.thickness;
706 this.crossSection = src.crossSection;
707 this.tabHeight = src.tabHeight;
708 this.tabLength = src.tabLength;
709 this.tabRelativePosition = src.tabRelativePosition;
710 this.tabShift = src.tabShift;
712 return super.copyFrom(c);