package net.sf.openrocket.rocketcomponent;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.List;
+import net.sf.openrocket.l10n.Translator;
+import net.sf.openrocket.startup.Application;
import net.sf.openrocket.util.Coordinate;
import net.sf.openrocket.util.MathUtil;
import net.sf.openrocket.util.Transformation;
public abstract class FinSet extends ExternalComponent {
+ private static final Translator trans = Application.getTranslator();
+
+ // FIXME: converting triangular fins to freeform fails
/**
* Maximum allowed cant of fins.
public enum CrossSection {
- SQUARE("Square", 1.00),
- ROUNDED("Rounded", 0.99),
- AIRFOIL("Airfoil", 0.85);
+ //// Square
+ SQUARE(trans.get("FinSet.CrossSection.SQUARE"), 1.00),
+ //// Rounded
+ ROUNDED(trans.get("FinSet.CrossSection.ROUNDED"), 0.99),
+ //// Airfoil
+ AIRFOIL(trans.get("FinSet.CrossSection.AIRFOIL"), 0.85);
private final String name;
private final double volume;
+
CrossSection(String name, double volume) {
this.name = name;
this.volume = volume;
public double getRelativeVolume() {
return volume;
}
+
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+
+ public enum TabRelativePosition {
+ //// Root chord leading edge
+ FRONT(trans.get("FinSet.TabRelativePosition.FRONT")),
+ //// Root chord midpoint
+ CENTER(trans.get("FinSet.TabRelativePosition.CENTER")),
+ //// Root chord trailing edge
+ END(trans.get("FinSet.TabRelativePosition.END"));
+
+ private final String name;
+
+ TabRelativePosition(String name) {
+ this.name = name;
+ }
+
@Override
public String toString() {
return name;
/**
* Rotation about the x-axis by 2*PI/fins.
*/
- protected Transformation finRotation = Transformation.rotate_x(2*Math.PI/fins);
+ protected Transformation finRotation = Transformation.rotate_x(2 * Math.PI / fins);
/**
* Rotation angle of the first fin. Zero corresponds to the positive y-axis.
*/
protected Transformation baseRotation = Transformation.rotate_x(rotation);
-
+
/**
* Cant angle of fins.
*/
/**
* Thickness of the fins.
*/
- protected double thickness = 0;
-
+ protected double thickness = 0.003;
+
/**
* The cross-section shape of the fins.
*/
protected CrossSection crossSection = CrossSection.SQUARE;
+
+ /*
+ * Fin tab properties.
+ */
+ private double tabHeight = 0;
+ private double tabLength = 0.05;
+ private double tabShift = 0;
+ private TabRelativePosition tabRelativePosition = TabRelativePosition.CENTER;
+
// Cached fin area & CG. Validity of both must be checked using finArea!
+ // Fin area does not include fin tabs, CG does.
private double finArea = -1;
private double finCGx = -1;
private double finCGy = -1;
public FinSet() {
super(RocketComponent.Position.BOTTOM);
}
-
+
/**
* Return the number of fins in the set.
* @return The number of fins.
public int getFinCount() {
return fins;
}
-
+
/**
* Sets the number of fins in the set.
* @param n The number of fins, greater of equal to one.
if (n > 8)
n = 8;
fins = n;
- finRotation = Transformation.rotate_x(2*Math.PI/fins);
+ finRotation = Transformation.rotate_x(2 * Math.PI / fins);
fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
}
public Transformation getFinRotationTransformation() {
return finRotation;
}
-
+
/**
* Gets the base rotation amount of the first fin.
* @return The base rotation amount.
baseRotation = Transformation.rotate_x(rotation);
fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
}
-
+
public Transformation getBaseRotationTransformation() {
return baseRotation;
}
-
+
public double getCantAngle() {
return cantAngle;
}
public Transformation getCantRotation() {
if (cantRotation == null) {
- if (MathUtil.equals(cantAngle,0)) {
+ if (MathUtil.equals(cantAngle, 0)) {
cantRotation = Transformation.IDENTITY;
} else {
- Transformation t = new Transformation(-length/2,0,0);
+ Transformation t = new Transformation(-length / 2, 0, 0);
t = Transformation.rotate_y(cantAngle).applyTransformation(t);
- t = new Transformation(length/2,0,0).applyTransformation(t);
+ t = new Transformation(length / 2, 0, 0).applyTransformation(t);
cantRotation = t;
}
}
public void setThickness(double r) {
if (thickness == r)
return;
- thickness = Math.max(r,0);
+ thickness = Math.max(r, 0);
fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
}
}
-
-
+
+
@Override
public void setRelativePosition(RocketComponent.Position position) {
super.setRelativePosition(position);
fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
}
-
+
@Override
public void setPositionValue(double value) {
super.setPositionValue(value);
fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
}
+
+
+
+ public double getTabHeight() {
+ return tabHeight;
+ }
+
+ public void setTabHeight(double height) {
+ height = MathUtil.max(height, 0);
+ if (MathUtil.equals(this.tabHeight, height))
+ return;
+ this.tabHeight = height;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+ public double getTabLength() {
+ return tabLength;
+ }
+
+ public void setTabLength(double length) {
+ length = MathUtil.max(length, 0);
+ if (MathUtil.equals(this.tabLength, length))
+ return;
+ this.tabLength = length;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+ public double getTabShift() {
+ return tabShift;
+ }
+
+ public void setTabShift(double shift) {
+ this.tabShift = shift;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+ public TabRelativePosition getTabRelativePosition() {
+ return tabRelativePosition;
+ }
+
+ public void setTabRelativePosition(TabRelativePosition position) {
+ if (this.tabRelativePosition == position)
+ return;
+
+ double front = getTabFrontEdge();
+ switch (position) {
+ case FRONT:
+ this.tabShift = front;
+ break;
+
+ case CENTER:
+ this.tabShift = front + tabLength / 2 - getLength() / 2;
+ break;
+
+ case END:
+ this.tabShift = front + tabLength - getLength();
+ break;
+
+ default:
+ throw new IllegalArgumentException("position=" + position);
+ }
+ this.tabRelativePosition = position;
+
+ fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+ }
+ /**
+ * Return the tab front edge position from the front of the fin.
+ */
+ public double getTabFrontEdge() {
+ switch (this.tabRelativePosition) {
+ case FRONT:
+ return tabShift;
+
+ case CENTER:
+ return getLength() / 2 - tabLength / 2 + tabShift;
+
+ case END:
+ return getLength() - tabLength + tabShift;
+
+ default:
+ throw new IllegalStateException("tabRelativePosition=" + tabRelativePosition);
+ }
+ }
+ /**
+ * Return the tab trailing edge position *from the front of the fin*.
+ */
+ public double getTabTrailingEdge() {
+ switch (this.tabRelativePosition) {
+ case FRONT:
+ return tabLength + tabShift;
+ case CENTER:
+ return getLength() / 2 + tabLength / 2 + tabShift;
+
+ case END:
+ return getLength() + tabShift;
+
+ default:
+ throw new IllegalStateException("tabRelativePosition=" + tabRelativePosition);
+ }
+ }
+
+
+
+
/////////// Calculation methods ///////////
/**
- * Return the area of one side of one fin.
+ * Return the area of one side of one fin. This does NOT include the area of
+ * the fin tab.
*
* @return the area of one side of one fin.
*/
return finArea;
}
+
/**
* Return the unweighted CG of a single fin. The X-coordinate is relative to
* the root chord trailing edge and the Y-coordinate to the fin root chord.
if (finArea < 0)
calculateAreaCG();
- return new Coordinate(finCGx,finCGy,0);
+ return new Coordinate(finCGx, finCGy, 0);
}
@Override
public double getComponentVolume() {
- return fins * getFinArea() * thickness * crossSection.getRelativeVolume();
+ return fins * (getFinArea() + tabHeight * tabLength) * thickness *
+ crossSection.getRelativeVolume();
}
-
+
@Override
public Coordinate getComponentCG() {
if (finArea < 0)
calculateAreaCG();
- double mass = getComponentMass(); // safe
+ double mass = getComponentMass(); // safe
if (fins == 1) {
return baseRotation.transform(
- new Coordinate(finCGx,finCGy + getBodyRadius(), 0, mass));
+ new Coordinate(finCGx, finCGy + getBodyRadius(), 0, mass));
} else {
return new Coordinate(finCGx, 0, 0, mass);
}
}
-
+
private void calculateAreaCG() {
Coordinate[] points = this.getFinPoints();
finCGx = 0;
finCGy = 0;
- for (int i=0; i < points.length-1; i++) {
+ for (int i = 0; i < points.length - 1; i++) {
final double x0 = points[i].x;
- final double x1 = points[i+1].x;
+ final double x1 = points[i + 1].x;
final double y0 = points[i].y;
- final double y1 = points[i+1].y;
+ final double y1 = points[i + 1].y;
- double da = (y0+y1)*(x1-x0) / 2;
+ double da = (y0 + y1) * (x1 - x0) / 2;
finArea += da;
- if (Math.abs(y0-y1) < 0.00001) {
- finCGx += (x0+x1)/2 * da;
- finCGy += y0/2 * da;
+ if (Math.abs(y0 - y1) < 0.00001) {
+ finCGx += (x0 + x1) / 2 * da;
+ finCGy += y0 / 2 * da;
} else {
- finCGx += (x0*(2*y0 + y1) + x1*(y0 + 2*y1)) / (3*(y0 + y1)) * da;
- finCGy += (y1 + y0*y0/(y0 + y1))/3 * da;
+ finCGx += (x0 * (2 * y0 + y1) + x1 * (y0 + 2 * y1)) / (3 * (y0 + y1)) * da;
+ finCGy += (y1 + y0 * y0 / (y0 + y1)) / 3 * da;
}
}
if (finArea < 0)
finArea = 0;
- if (finArea > 0) {
- finCGx /= finArea;
- finCGy /= finArea;
+ // Add effect of fin tabs to CG
+ double tabArea = tabLength * tabHeight;
+ if (!MathUtil.equals(tabArea, 0)) {
+
+ double x = (getTabFrontEdge() + getTabTrailingEdge()) / 2;
+ double y = -this.tabHeight / 2;
+
+ finCGx += x * tabArea;
+ finCGy += y * tabArea;
+
+ }
+
+ if ((finArea + tabArea) > 0) {
+ finCGx /= (finArea + tabArea);
+ finCGy /= (finArea + tabArea);
} else {
- finCGx = (points[0].x + points[points.length-1].x)/2;
+ finCGx = (points[0].x + points[points.length - 1].x) / 2;
finCGy = 0;
}
}
/*
- * Return an approximation of the longitudal unitary inertia of the fin set.
+ * Return an approximation of the longitudinal unitary inertia of the fin set.
* The process is the following:
*
* 1. Approximate the fin with a rectangular fin
* set and multiplied by the number of fins.
*/
@Override
- public double getLongitudalUnitInertia() {
+ public double getLongitudinalUnitInertia() {
double area = getFinArea();
if (MathUtil.equals(area, 0))
return 0;
// w2 and h2 are squares of the fin width and height
double w = getLength();
double h = getSpan();
- double w2,h2;
+ double w2, h2;
- if (MathUtil.equals(w*h,0)) {
+ if (MathUtil.equals(w * h, 0)) {
w2 = area;
h2 = area;
} else {
- w2 = w*area/h;
- h2 = h*area/w;
+ w2 = w * area / h;
+ h2 = h * area / w;
}
- double inertia = (h2 + 2*w2)/24;
+ double inertia = (h2 + 2 * w2) / 24;
if (fins == 1)
return inertia;
double radius = getBodyRadius();
-
+
return fins * (inertia + MathUtil.pow2(Math.sqrt(h2) + radius));
}
double w = getLength();
double h = getSpan();
- if (MathUtil.equals(w*h,0)) {
+ if (MathUtil.equals(w * h, 0)) {
h = Math.sqrt(area);
} else {
- h = Math.sqrt(h*area/w);
+ h = Math.sqrt(h * area / w);
}
if (fins == 1)
- return h*h / 12;
+ return h * h / 12;
double radius = getBodyRadius();
- return fins * (h*h/12 + MathUtil.pow2(h/2 + radius));
+ return fins * (h * h / 12 + MathUtil.pow2(h / 2 + radius));
}
List<Coordinate> bounds = new ArrayList<Coordinate>();
double r = getBodyRadius();
- for (Coordinate point: getFinPoints()) {
+ for (Coordinate point : getFinPoints()) {
addFinBound(bounds, point.x, point.y + r);
}
return bounds;
}
-
+
/**
* Adds the 2d-coordinate bound (x,y) to the collection for both z-components and for
Coordinate c;
int i;
- c = new Coordinate(x,y,thickness/2);
+ c = new Coordinate(x, y, thickness / 2);
c = baseRotation.transform(c);
set.add(c);
- for (i=1; i<fins; i++) {
+ for (i = 1; i < fins; i++) {
c = finRotation.transform(c);
set.add(c);
}
- c = new Coordinate(x,y,-thickness/2);
+ c = new Coordinate(x, y, -thickness / 2);
c = baseRotation.transform(c);
set.add(c);
- for (i=1; i<fins; i++) {
+ for (i = 1; i < fins; i++) {
c = finRotation.transform(c);
set.add(c);
}
}
-
+
@Override
public void componentChanged(ComponentChangeEvent e) {
if (e.isAerodynamicChange()) {
RocketComponent s;
s = this.getParent();
- while (s!=null) {
+ while (s != null) {
if (s instanceof SymmetricComponent) {
- double x = this.toRelative(new Coordinate(0,0,0), s)[0].x;
- return ((SymmetricComponent)s).getRadius(x);
+ double x = this.toRelative(new Coordinate(0, 0, 0), s)[0].x;
+ return ((SymmetricComponent) s).getRadius(x);
}
s = s.getParent();
}
return 0;
}
+ @Override
+ public boolean allowsChildren() {
+ return false;
+ }
+
/**
* Allows nothing to be attached to a FinSet.
*
}
-
-
+
+
/**
* Return a list of coordinates defining the geometry of a single fin.
* The coordinates are the XY-coordinates of points defining the shape of a single fin,
*/
public abstract Coordinate[] getFinPoints();
+
+ /**
+ * Return a list of coordinates defining the geometry of a single fin, including a
+ * possible fin tab. The coordinates are the XY-coordinates of points defining the
+ * shape of a single fin, where the origin is the leading root edge. This implementation
+ * calls {@link #getFinPoints()} and adds the necessary points for the fin tab.
+ * The tab coordinates will have a negative y value.
+ *
+ * @return List of XY-coordinates.
+ */
+ public Coordinate[] getFinPointsWithTab() {
+ Coordinate[] points = getFinPoints();
+
+ if (MathUtil.equals(getTabHeight(), 0) ||
+ MathUtil.equals(getTabLength(), 0))
+ return points;
+
+ double x1 = getTabFrontEdge();
+ double x2 = getTabTrailingEdge();
+ double y = -getTabHeight();
+
+ int n = points.length;
+ points = Arrays.copyOf(points, points.length + 4);
+ points[n] = new Coordinate(x2, 0);
+ points[n + 1] = new Coordinate(x2, y);
+ points[n + 2] = new Coordinate(x1, y);
+ points[n + 3] = new Coordinate(x1, 0);
+ return points;
+ }
+
+
+
/**
* Get the span of a single fin. That is, the length from the root to the tip of the fin.
* @return Span of a single fin.
@Override
- protected void copyFrom(RocketComponent c) {
- super.copyFrom(c);
-
- FinSet src = (FinSet)c;
+ protected List<RocketComponent> copyFrom(RocketComponent c) {
+ FinSet src = (FinSet) c;
this.fins = src.fins;
this.finRotation = src.finRotation;
this.rotation = src.rotation;
this.cantRotation = src.cantRotation;
this.thickness = src.thickness;
this.crossSection = src.crossSection;
+ this.tabHeight = src.tabHeight;
+ this.tabLength = src.tabLength;
+ this.tabRelativePosition = src.tabRelativePosition;
+ this.tabShift = src.tabShift;
+
+ return super.copyFrom(c);
}
}