1 package net.sf.openrocket.rocketcomponent;
3 import net.sf.openrocket.util.Coordinate;
4 import net.sf.openrocket.util.MathUtil;
6 import java.util.Collection;
8 import static java.lang.Math.sin;
9 import static java.lang.Math.sqrt;
10 import static net.sf.openrocket.util.Chars.FRAC12;
11 import static net.sf.openrocket.util.Chars.FRAC34;
12 import static net.sf.openrocket.util.MathUtil.pow2;
13 import static net.sf.openrocket.util.MathUtil.pow3;
16 public class Transition extends SymmetricComponent {
17 private static final double CLIP_PRECISION = 0.0001;
21 private double shapeParameter;
22 private boolean clipped; // Not to be read - use isClipped(), which may be overriden
24 private double radius1, radius2;
25 private boolean autoRadius1, autoRadius2; // Whether the start radius is automatic
28 private double foreShoulderRadius;
29 private double foreShoulderThickness;
30 private double foreShoulderLength;
31 private boolean foreShoulderCapped;
32 private double aftShoulderRadius;
33 private double aftShoulderThickness;
34 private double aftShoulderLength;
35 private boolean aftShoulderCapped;
38 // Used to cache the clip length
39 private double clipLength = -1;
44 this.radius1 = DEFAULT_RADIUS;
45 this.radius2 = DEFAULT_RADIUS;
46 this.length = DEFAULT_RADIUS * 3;
47 this.autoRadius1 = true;
48 this.autoRadius2 = true;
50 this.type = Shape.CONICAL;
51 this.shapeParameter = 0;
58 //////// Fore radius ////////
62 public double getForeRadius() {
63 if (isForeRadiusAutomatic()) {
64 // Get the automatic radius from the front
66 SymmetricComponent c = this.getPreviousSymmetricComponent();
68 r = c.getFrontAutoRadius();
77 public void setForeRadius(double radius) {
78 if ((this.radius1 == radius) && (autoRadius1 == false))
81 this.autoRadius1 = false;
82 this.radius1 = Math.max(radius, 0);
84 if (this.thickness > this.radius1 && this.thickness > this.radius2)
85 this.thickness = Math.max(this.radius1, this.radius2);
86 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
90 public boolean isForeRadiusAutomatic() {
94 public void setForeRadiusAutomatic(boolean auto) {
95 if (autoRadius1 == auto)
99 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
103 //////// Aft radius /////////
106 public double getAftRadius() {
107 if (isAftRadiusAutomatic()) {
108 // Return the auto radius from the rear
110 SymmetricComponent c = this.getNextSymmetricComponent();
112 r = c.getRearAutoRadius();
123 public void setAftRadius(double radius) {
124 if ((this.radius2 == radius) && (autoRadius2 == false))
127 this.autoRadius2 = false;
128 this.radius2 = Math.max(radius, 0);
130 if (this.thickness > this.radius1 && this.thickness > this.radius2)
131 this.thickness = Math.max(this.radius1, this.radius2);
132 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
136 public boolean isAftRadiusAutomatic() {
140 public void setAftRadiusAutomatic(boolean auto) {
141 if (autoRadius2 == auto)
145 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
150 //// Radius automatics
153 protected double getFrontAutoRadius() {
154 if (isAftRadiusAutomatic())
156 return getAftRadius();
161 protected double getRearAutoRadius() {
162 if (isForeRadiusAutomatic())
164 return getForeRadius();
170 //////// Type & shape /////////
172 public Shape getType() {
176 public void setType(Shape type) {
178 throw new IllegalArgumentException("setType called with null argument");
180 if (this.type == type)
183 this.clipped = type.isClippable();
184 this.shapeParameter = type.defaultParameter();
185 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
188 public double getShapeParameter() {
189 return shapeParameter;
192 public void setShapeParameter(double n) {
193 if (shapeParameter == n)
195 this.shapeParameter = MathUtil.clamp(n, type.minParameter(), type.maxParameter());
196 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
199 public boolean isClipped() {
200 if (!type.isClippable())
205 public void setClipped(boolean c) {
209 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
212 public boolean isClippedEnabled() {
213 return type.isClippable();
216 public double getShapeParameterMin() {
217 return type.minParameter();
220 public double getShapeParameterMax() {
221 return type.maxParameter();
225 //////// Shoulders ////////
227 public double getForeShoulderRadius() {
228 return foreShoulderRadius;
231 public void setForeShoulderRadius(double foreShoulderRadius) {
232 if (MathUtil.equals(this.foreShoulderRadius, foreShoulderRadius))
234 this.foreShoulderRadius = foreShoulderRadius;
235 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
238 public double getForeShoulderThickness() {
239 return foreShoulderThickness;
242 public void setForeShoulderThickness(double foreShoulderThickness) {
243 if (MathUtil.equals(this.foreShoulderThickness, foreShoulderThickness))
245 this.foreShoulderThickness = foreShoulderThickness;
246 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
249 public double getForeShoulderLength() {
250 return foreShoulderLength;
253 public void setForeShoulderLength(double foreShoulderLength) {
254 if (MathUtil.equals(this.foreShoulderLength, foreShoulderLength))
256 this.foreShoulderLength = foreShoulderLength;
257 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
260 public boolean isForeShoulderCapped() {
261 return foreShoulderCapped;
264 public void setForeShoulderCapped(boolean capped) {
265 if (this.foreShoulderCapped == capped)
267 this.foreShoulderCapped = capped;
268 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
274 public double getAftShoulderRadius() {
275 return aftShoulderRadius;
278 public void setAftShoulderRadius(double aftShoulderRadius) {
279 if (MathUtil.equals(this.aftShoulderRadius, aftShoulderRadius))
281 this.aftShoulderRadius = aftShoulderRadius;
282 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
285 public double getAftShoulderThickness() {
286 return aftShoulderThickness;
289 public void setAftShoulderThickness(double aftShoulderThickness) {
290 if (MathUtil.equals(this.aftShoulderThickness, aftShoulderThickness))
292 this.aftShoulderThickness = aftShoulderThickness;
293 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
296 public double getAftShoulderLength() {
297 return aftShoulderLength;
300 public void setAftShoulderLength(double aftShoulderLength) {
301 if (MathUtil.equals(this.aftShoulderLength, aftShoulderLength))
303 this.aftShoulderLength = aftShoulderLength;
304 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
307 public boolean isAftShoulderCapped() {
308 return aftShoulderCapped;
311 public void setAftShoulderCapped(boolean capped) {
312 if (this.aftShoulderCapped == capped)
314 this.aftShoulderCapped = capped;
315 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
321 /////////// Shape implementations ////////////
326 * Return the radius at point x of the transition.
329 public double getRadius(double x) {
330 if (x < 0 || x > length)
333 double r1 = getForeRadius();
334 double r2 = getAftRadius();
347 // Check clip calculation
349 calculateClip(r1, r2);
350 return type.getRadius(clipLength + x, r2, clipLength + length, shapeParameter);
353 return r1 + type.getRadius(x, r2 - r1, length, shapeParameter);
358 * Numerically solve clipLength from the equation
359 * r1 == type.getRadius(clipLength,r2,clipLength+length)
360 * using a binary search. It assumes getOuterRadius() to be monotonically increasing.
362 private void calculateClip(double r1, double r2) {
363 double min = 0, max = length;
382 // getR(min,min+length,r2) - r1 < 0
383 // getR(max,max+length,r2) - r1 > 0
386 while (type.getRadius(max, r2, max + length, shapeParameter) - r1 < 0) {
395 clipLength = (min + max) / 2;
396 if ((max - min) < CLIP_PRECISION)
398 double val = type.getRadius(clipLength, r2, clipLength + length, shapeParameter);
409 public double getInnerRadius(double x) {
410 return Math.max(getRadius(x) - thickness, 0);
416 public Collection<Coordinate> getComponentBounds() {
417 Collection<Coordinate> bounds = super.getComponentBounds();
418 if (foreShoulderLength > 0.001)
419 addBound(bounds, -foreShoulderLength, foreShoulderRadius);
420 if (aftShoulderLength > 0.001)
421 addBound(bounds, getLength() + aftShoulderLength, aftShoulderRadius);
426 public double getComponentMass() {
427 double mass = super.getComponentMass();
428 if (getForeShoulderLength() > 0.001) {
429 final double or = getForeShoulderRadius();
430 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
431 mass += ringMass(or, ir, getForeShoulderLength(), getMaterial().getDensity());
433 if (isForeShoulderCapped()) {
434 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
435 mass += ringMass(ir, 0, getForeShoulderThickness(), getMaterial().getDensity());
438 if (getAftShoulderLength() > 0.001) {
439 final double or = getAftShoulderRadius();
440 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
441 mass += ringMass(or, ir, getAftShoulderLength(), getMaterial().getDensity());
443 if (isAftShoulderCapped()) {
444 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
445 mass += ringMass(ir, 0, getAftShoulderThickness(), getMaterial().getDensity());
452 public Coordinate getComponentCG() {
453 Coordinate cg = super.getComponentCG();
454 if (getForeShoulderLength() > 0.001) {
455 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
456 cg = cg.average(ringCG(getForeShoulderRadius(), ir, -getForeShoulderLength(), 0,
457 getMaterial().getDensity()));
459 if (isForeShoulderCapped()) {
460 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
461 cg = cg.average(ringCG(ir, 0, -getForeShoulderLength(),
462 getForeShoulderThickness() - getForeShoulderLength(),
463 getMaterial().getDensity()));
466 if (getAftShoulderLength() > 0.001) {
467 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
468 cg = cg.average(ringCG(getAftShoulderRadius(), ir, getLength(),
469 getLength() + getAftShoulderLength(), getMaterial().getDensity()));
471 if (isAftShoulderCapped()) {
472 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
473 cg = cg.average(ringCG(ir, 0,
474 getLength() + getAftShoulderLength() - getAftShoulderThickness(),
475 getLength() + getAftShoulderLength(), getMaterial().getDensity()));
482 * The moments of inertia are not explicitly corrected for the shoulders.
483 * However, since the mass is corrected, the inertia is automatically corrected
484 * to very nearly the correct value.
490 * Returns the name of the component ("Transition").
493 public String getComponentName() {
498 protected void componentChanged(ComponentChangeEvent e) {
499 super.componentChanged(e);
504 * Check whether the given type can be added to this component. Transitions allow any
505 * InternalComponents to be added.
507 * @param type The RocketComponent class type to add.
508 * @return Whether such a component can be added.
511 public boolean isCompatible(Class<? extends RocketComponent> type) {
512 if (InternalComponent.class.isAssignableFrom(type))
520 * An enumeration listing the possible shapes of transitions.
522 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
524 public static enum Shape {
530 "A conical nose cone has a profile of a triangle.",
531 "A conical transition has straight sides.") {
533 public double getRadius(double x, double radius, double length, double param) {
537 return radius * x / length;
542 * Ogive shape. The shape parameter is the portion of an extended tangent ogive
543 * that will be used. That is, for param==1 a tangent ogive will be produced, and
544 * for smaller values the shape straightens out into a cone at param==0.
547 "An ogive nose cone has a profile that is a segment of a circle. " +
548 "The shape parameter value 1 produces a <b>tangent ogive</b>, which has " +
549 "a smooth transition to the body tube, values less than 1 produce " +
550 "<b>secant ogives</b>.",
551 "An ogive transition has a profile that is a segment of a circle. " +
552 "The shape parameter value 1 produces a <b>tangent ogive</b>, which has " +
553 "a smooth transition to the body tube at the aft end, values less than 1 " +
554 "produce <b>secant ogives</b>.") {
556 public boolean usesParameter() {
557 return true; // Range 0...1 is default
561 public double defaultParameter() {
562 return 1.0; // Tangent ogive by default
566 public double getRadius(double x, double radius, double length, double param) {
573 // Impossible to calculate ogive for length < radius, scale instead
574 // TODO: LOW: secant ogive could be calculated lower
575 if (length < radius) {
576 x = x * radius / length;
581 return CONICAL.getRadius(x, radius, length, param);
583 // Radius of circle is:
584 double R = sqrt((pow2(length) + pow2(radius)) *
585 (pow2((2 - param) * length) + pow2(param * radius)) / (4 * pow2(param * radius)));
586 double L = length / param;
587 // double R = (radius + length*length/(radius*param*param))/2;
588 double y0 = sqrt(R * R - L * L);
589 return sqrt(R * R - (L - x) * (L - x)) - y0;
596 ELLIPSOID("Ellipsoid",
597 "An ellipsoidal nose cone has a profile of a half-ellipse " +
598 "with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>.",
599 "An ellipsoidal transition has a profile of a half-ellipse " +
600 "with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>. If the " +
601 "transition is not clipped, then the profile is extended at the center by the " +
602 "corresponding radius.", true) {
604 public double getRadius(double x, double radius, double length, double param) {
608 x = x * radius / length;
609 return sqrt(2 * radius * x - x * x); // radius/length * sphere
613 POWER("Power series",
614 "A power series nose cone has a profile of " +
615 "<i>Radius</i> × (<i>x</i> / <i>Length</i>)" +
616 "<sup><i>k</i></sup> " +
617 "where <i>k</i> is the shape parameter. For <i>k</i>=0.5 this is a " +
618 "<b>" + FRAC12 + "-power</b> or <b>parabolic</b> nose cone, for <i>k</i>=0.75 a " +
619 "<b>" + FRAC34 + "-power</b>, and for <i>k</i>=1 a <b>conical</b> nose cone.",
620 "A power series transition has a profile of " +
621 "<i>Radius</i> × (<i>x</i> / <i>Length</i>)" +
622 "<sup><i>k</i></sup> " +
623 "where <i>k</i> is the shape parameter. For <i>k</i>=0.5 the transition is " +
624 "<b>" + FRAC12 + "-power</b> or <b>parabolic</b>, for <i>k</i>=0.75 a " +
625 "<b>" + FRAC34 + "-power</b>, and for <i>k</i>=1 <b>conical</b>.", true) {
627 public boolean usesParameter() { // Range 0...1
632 public double defaultParameter() {
637 public double getRadius(double x, double radius, double length, double param) {
643 if (param <= 0.00001) {
649 return radius * Math.pow(x / length, param);
654 PARABOLIC("Parabolic series",
655 "A parabolic series nose cone has a profile of a parabola. The shape " +
656 "parameter defines the segment of the parabola to utilize. The shape " +
657 "parameter 1.0 produces a <b>full parabola</b> which is tangent to the body " +
658 "tube, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a " +
659 "<b>1/2 parabola</b> and 0 produces a <b>conical</b> nose cone.",
660 "A parabolic series transition has a profile of a parabola. The shape " +
661 "parameter defines the segment of the parabola to utilize. The shape " +
662 "parameter 1.0 produces a <b>full parabola</b> which is tangent to the body " +
663 "tube at the aft end, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a " +
664 "<b>1/2 parabola</b> and 0 produces a <b>conical</b> transition.") {
666 // In principle a parabolic transition is clippable, but the difference is
670 public boolean usesParameter() { // Range 0...1
675 public double defaultParameter() {
680 public double getRadius(double x, double radius, double length, double param) {
687 return radius * ((2 * x / length - param * pow2(x / length)) / (2 - param));
693 HAACK("Haack series",
694 "The Haack series nose cones are designed to minimize drag. The shape parameter " +
695 "0 produces an <b>LD-Haack</b> or <b>Von Karman</b> nose cone, which minimizes " +
696 "drag for fixed length and diameter, while a value of 0.333 produces an " +
697 "<b>LV-Haack</b> nose cone, which minimizes drag for fixed length and volume.",
698 "The Haack series <i>nose cones</i> are designed to minimize drag. " +
699 "These transition shapes are their equivalents, but do not necessarily produce " +
700 "optimal drag for transitions. " +
701 "The shape parameter 0 produces an <b>LD-Haack</b> or <b>Von Karman</b> shape, " +
702 "while a value of 0.333 produces an <b>LV-Haack</b> shape.", true) {
704 public boolean usesParameter() {
709 public double maxParameter() {
710 return 1.0 / 3.0; // Range 0...1/3
714 public double getRadius(double x, double radius, double length, double param) {
721 double theta = Math.acos(1 - 2 * x / length);
723 return radius * sqrt((theta - sin(2 * theta) / 2) / Math.PI);
725 return radius * sqrt((theta - sin(2 * theta) / 2 + param * pow3(sin(theta))) / Math.PI);
729 // POLYNOMIAL("Smooth polynomial",
730 // "A polynomial is fitted such that the nose cone profile is horizontal "+
731 // "at the aft end of the transition. The angle at the tip is defined by "+
732 // "the shape parameter.",
733 // "A polynomial is fitted such that the transition profile is horizontal "+
734 // "at the aft end of the transition. The angle at the fore end is defined "+
735 // "by the shape parameter.") {
737 // public boolean usesParameter() {
741 // public double maxParameter() {
742 // return 3.0; // Range 0...3
745 // public double defaultParameter() {
748 // public double getRadius(double x, double radius, double length, double param) {
750 // assert x <= length;
751 // assert radius >= 0;
752 // assert param >= 0;
753 // assert param <= 3;
754 // // p(x) = (k-2)x^3 + (3-2k)x^2 + k*x
756 // return radius*((((param-2)*x + (3-2*param))*x + param)*x);
761 // Privete fields of the shapes
762 private final String name;
763 private final String transitionDesc;
764 private final String noseconeDesc;
765 private final boolean canClip;
767 // Non-clippable constructor
768 Shape(String name, String noseconeDesc, String transitionDesc) {
769 this(name, noseconeDesc, transitionDesc, false);
772 // Clippable constructor
773 Shape(String name, String noseconeDesc, String transitionDesc, boolean canClip) {
775 this.canClip = canClip;
776 this.noseconeDesc = noseconeDesc;
777 this.transitionDesc = transitionDesc;
782 * Return the name of the transition shape name.
784 public String getName() {
789 * Get a description of the Transition shape.
791 public String getTransitionDescription() {
792 return transitionDesc;
796 * Get a description of the NoseCone shape.
798 public String getNoseConeDescription() {
803 * Check whether the shape differs in clipped mode. The clipping should be
804 * enabled by default if possible.
806 public boolean isClippable() {
811 * Return whether the shape uses the shape parameter. (Default false.)
813 public boolean usesParameter() {
818 * Return the minimum value of the shape parameter. (Default 0.)
820 public double minParameter() {
825 * Return the maximum value of the shape parameter. (Default 1.)
827 public double maxParameter() {
832 * Return the default value of the shape parameter. (Default 0.)
834 public double defaultParameter() {
839 * Calculate the basic radius of a transition with the given radius, length and
840 * shape parameter at the point x from the tip of the component. It is assumed
841 * that the fore radius if zero and the aft radius is <code>radius >= 0</code>.
842 * Boattails are achieved by reversing the component.
844 * @param x Position from the tip of the component.
845 * @param radius Aft end radius >= 0.
846 * @param length Length of the transition >= 0.
847 * @param param Valid shape parameter.
848 * @return The basic radius at the given position.
850 public abstract double getRadius(double x, double radius, double length, double param);
854 * Returns the name of the shape (same as getName()).
857 public String toString() {