1 package net.sf.openrocket.rocketcomponent;
3 import static java.lang.Math.sin;
4 import static java.lang.Math.sqrt;
5 import static net.sf.openrocket.util.MathUtil.pow2;
6 import static net.sf.openrocket.util.MathUtil.pow3;
8 import java.util.Collection;
10 import net.sf.openrocket.util.Coordinate;
11 import net.sf.openrocket.util.MathUtil;
14 public class Transition extends SymmetricComponent {
15 private static final double CLIP_PRECISION = 0.0001;
19 private double shapeParameter;
20 private boolean clipped; // Not to be read - use isClipped(), which may be overriden
22 private double radius1, radius2;
23 private boolean autoRadius1, autoRadius2; // Whether the start radius is automatic
26 private double foreShoulderRadius;
27 private double foreShoulderThickness;
28 private double foreShoulderLength;
29 private boolean foreShoulderCapped;
30 private double aftShoulderRadius;
31 private double aftShoulderThickness;
32 private double aftShoulderLength;
33 private boolean aftShoulderCapped;
36 // Used to cache the clip length
37 private double clipLength=-1;
42 this.radius1 = DEFAULT_RADIUS;
43 this.radius2 = DEFAULT_RADIUS;
44 this.length = DEFAULT_RADIUS * 3;
45 this.autoRadius1 = true;
46 this.autoRadius2 = true;
48 this.type = Shape.CONICAL;
49 this.shapeParameter = 0;
56 //////// Fore radius ////////
60 public double getForeRadius() {
61 if (isForeRadiusAutomatic()) {
62 // Get the automatic radius from the front
64 SymmetricComponent c = this.getPreviousSymmetricComponent();
66 r = c.getFrontAutoRadius();
75 public void setForeRadius(double radius) {
76 if ((this.radius1 == radius) && (autoRadius1 == false))
79 this.autoRadius1 = false;
80 this.radius1 = Math.max(radius,0);
82 if (this.thickness > this.radius1 && this.thickness > this.radius2)
83 this.thickness = Math.max(this.radius1, this.radius2);
84 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
88 public boolean isForeRadiusAutomatic() {
92 public void setForeRadiusAutomatic(boolean auto) {
93 if (autoRadius1 == auto)
97 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
101 //////// Aft radius /////////
104 public double getAftRadius() {
105 if (isAftRadiusAutomatic()) {
106 // Return the auto radius from the rear
108 SymmetricComponent c = this.getNextSymmetricComponent();
110 r = c.getRearAutoRadius();
121 public void setAftRadius(double radius) {
122 if ((this.radius2 == radius) && (autoRadius2 == false))
125 this.autoRadius2 = false;
126 this.radius2 = Math.max(radius,0);
128 if (this.thickness > this.radius1 && this.thickness > this.radius2)
129 this.thickness = Math.max(this.radius1, this.radius2);
130 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
134 public boolean isAftRadiusAutomatic() {
138 public void setAftRadiusAutomatic(boolean auto) {
139 if (autoRadius2 == auto)
143 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
148 //// Radius automatics
151 protected double getFrontAutoRadius() {
152 if (isAftRadiusAutomatic())
154 return getAftRadius();
159 protected double getRearAutoRadius() {
160 if (isForeRadiusAutomatic())
162 return getForeRadius();
168 //////// Type & shape /////////
170 public Shape getType() {
174 public void setType(Shape type) {
175 if (this.type == type)
178 this.clipped = type.isClippable();
179 this.shapeParameter = type.defaultParameter();
180 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
183 public double getShapeParameter() {
184 return shapeParameter;
187 public void setShapeParameter(double n) {
188 if (shapeParameter == n)
190 this.shapeParameter = MathUtil.clamp(n, type.minParameter(), type.maxParameter());
191 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
194 public boolean isClipped() {
195 if (!type.isClippable())
200 public void setClipped(boolean c) {
204 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
207 public boolean isClippedEnabled() {
208 return type.isClippable();
211 public double getShapeParameterMin() {
212 return type.minParameter();
215 public double getShapeParameterMax() {
216 return type.maxParameter();
220 //////// Shoulders ////////
222 public double getForeShoulderRadius() {
223 return foreShoulderRadius;
226 public void setForeShoulderRadius(double foreShoulderRadius) {
227 if (MathUtil.equals(this.foreShoulderRadius, foreShoulderRadius))
229 this.foreShoulderRadius = foreShoulderRadius;
230 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
233 public double getForeShoulderThickness() {
234 return foreShoulderThickness;
237 public void setForeShoulderThickness(double foreShoulderThickness) {
238 if (MathUtil.equals(this.foreShoulderThickness, foreShoulderThickness))
240 this.foreShoulderThickness = foreShoulderThickness;
241 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
244 public double getForeShoulderLength() {
245 return foreShoulderLength;
248 public void setForeShoulderLength(double foreShoulderLength) {
249 if (MathUtil.equals(this.foreShoulderLength, foreShoulderLength))
251 this.foreShoulderLength = foreShoulderLength;
252 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
255 public boolean isForeShoulderCapped() {
256 return foreShoulderCapped;
259 public void setForeShoulderCapped(boolean capped) {
260 if (this.foreShoulderCapped == capped)
262 this.foreShoulderCapped = capped;
263 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
269 public double getAftShoulderRadius() {
270 return aftShoulderRadius;
273 public void setAftShoulderRadius(double aftShoulderRadius) {
274 if (MathUtil.equals(this.aftShoulderRadius, aftShoulderRadius))
276 this.aftShoulderRadius = aftShoulderRadius;
277 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
280 public double getAftShoulderThickness() {
281 return aftShoulderThickness;
284 public void setAftShoulderThickness(double aftShoulderThickness) {
285 if (MathUtil.equals(this.aftShoulderThickness, aftShoulderThickness))
287 this.aftShoulderThickness = aftShoulderThickness;
288 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
291 public double getAftShoulderLength() {
292 return aftShoulderLength;
295 public void setAftShoulderLength(double aftShoulderLength) {
296 if (MathUtil.equals(this.aftShoulderLength, aftShoulderLength))
298 this.aftShoulderLength = aftShoulderLength;
299 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
302 public boolean isAftShoulderCapped() {
303 return aftShoulderCapped;
306 public void setAftShoulderCapped(boolean capped) {
307 if (this.aftShoulderCapped == capped)
309 this.aftShoulderCapped = capped;
310 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
316 /////////// Shape implementations ////////////
321 * Return the radius at point x of the transition.
324 public double getRadius(double x) {
328 double r1=getForeRadius();
329 double r2=getAftRadius();
342 // Check clip calculation
344 calculateClip(r1,r2);
345 return type.getRadius(clipLength+x, r2, clipLength+length, shapeParameter);
348 return r1 + type.getRadius(x, r2-r1, length, shapeParameter);
353 * Numerically solve clipLength from the equation
354 * r1 == type.getRadius(clipLength,r2,clipLength+length)
355 * using a binary search. It assumes getRadius() to be monotonically increasing.
357 private void calculateClip(double r1, double r2) {
358 double min=0, max=length;
377 // getR(min,min+length,r2) - r1 < 0
378 // getR(max,max+length,r2) - r1 > 0
381 while (type.getRadius(max, r2, max+length, shapeParameter) - r1 < 0) {
390 clipLength = (min+max)/2;
391 if ((max-min)<CLIP_PRECISION)
393 double val = type.getRadius(clipLength, r2, clipLength+length, shapeParameter);
404 public double getInnerRadius(double x) {
405 return Math.max(getRadius(x)-thickness,0);
411 public Collection<Coordinate> getComponentBounds() {
412 Collection<Coordinate> bounds = super.getComponentBounds();
413 if (foreShoulderLength > 0.001)
414 addBound(bounds, -foreShoulderLength, foreShoulderRadius);
415 if (aftShoulderLength > 0.001)
416 addBound(bounds, getLength() + aftShoulderLength, aftShoulderRadius);
421 public double getComponentMass() {
422 double mass = super.getComponentMass();
423 if (getForeShoulderLength() > 0.001) {
424 final double or = getForeShoulderRadius();
425 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
426 mass += ringMass(or, ir, getForeShoulderLength(), getMaterial().getDensity());
428 if (isForeShoulderCapped()) {
429 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
430 mass += ringMass(ir, 0, getForeShoulderThickness(), getMaterial().getDensity());
433 if (getAftShoulderLength() > 0.001) {
434 final double or = getAftShoulderRadius();
435 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
436 mass += ringMass(or, ir, getAftShoulderLength(), getMaterial().getDensity());
438 if (isAftShoulderCapped()) {
439 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
440 mass += ringMass(ir, 0, getAftShoulderThickness(), getMaterial().getDensity());
447 public Coordinate getComponentCG() {
448 Coordinate cg = super.getComponentCG();
449 if (getForeShoulderLength() > 0.001) {
450 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
451 cg = cg.average(ringCG(getForeShoulderRadius(), ir, -getForeShoulderLength(), 0,
452 getMaterial().getDensity()));
454 if (isForeShoulderCapped()) {
455 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
456 cg = cg.average(ringCG(ir, 0, -getForeShoulderLength(),
457 getForeShoulderThickness()-getForeShoulderLength(),
458 getMaterial().getDensity()));
461 if (getAftShoulderLength() > 0.001) {
462 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
463 cg = cg.average(ringCG(getAftShoulderRadius(), ir, getLength(),
464 getLength()+getAftShoulderLength(), getMaterial().getDensity()));
466 if (isAftShoulderCapped()) {
467 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
468 cg = cg.average(ringCG(ir, 0,
469 getLength()+getAftShoulderLength()-getAftShoulderThickness(),
470 getLength()+getAftShoulderLength(), getMaterial().getDensity()));
477 * The moments of inertia are not explicitly corrected for the shoulders.
478 * However, since the mass is corrected, the inertia is automatically corrected
479 * to very nearly the correct value.
485 * Returns the name of the component ("Transition").
488 public String getComponentName() {
493 protected void componentChanged(ComponentChangeEvent e) {
494 super.componentChanged(e);
501 * An enumeration listing the possible shapes of transitions.
503 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
505 public static enum Shape {
511 "A conical nose cone has a profile of a triangle.",
512 "A conical transition has straight sides.") {
514 public double getRadius(double x, double radius, double length, double param) {
518 return radius*x/length;
523 * Ogive shape. The shape parameter is the portion of an extended tangent ogive
524 * that will be used. That is, for param==1 a tangent ogive will be produced, and
525 * for smaller values the shape straightens out into a cone at param==0.
528 "An ogive nose cone has a profile that is a segment of a circle. " +
529 "The shape parameter value 1 produces a <b>tangent ogive</b>, which has " +
530 "a smooth transition to the body tube, values less than 1 produce "+
531 "<b>secant ogives</b>.",
532 "An ogive transition has a profile that is a segment of a circle. " +
533 "The shape parameter value 1 produces a <b>tangent ogive</b>, which has " +
534 "a smooth transition to the body tube at the aft end, values less than 1 " +
535 "produce <b>secant ogives</b>.") {
537 public boolean usesParameter() {
538 return true; // Range 0...1 is default
541 public double defaultParameter() {
542 return 1.0; // Tangent ogive by default
545 public double getRadius(double x, double radius, double length, double param) {
552 // Impossible to calculate ogive for length < radius, scale instead
553 // TODO: LOW: secant ogive could be calculated lower
554 if (length < radius) {
555 x = x * radius / length;
560 return CONICAL.getRadius(x, radius, length, param);
562 // Radius of circle is:
563 double R = sqrt((pow2(length)+pow2(radius)) *
564 (pow2((2-param)*length) + pow2(param*radius))/(4*pow2(param*radius)));
565 double L = length/param;
566 // double R = (radius + length*length/(radius*param*param))/2;
567 double y0 = sqrt(R*R - L*L);
568 return sqrt(R*R - (L-x)*(L-x)) - y0;
575 ELLIPSOID("Ellipsoid",
576 "An ellipsoidal nose cone has a profile of a half-ellipse "+
577 "with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>.",
578 "An ellipsoidal transition has a profile of a half-ellipse "+
579 "with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>. If the "+
580 "transition is not clipped, then the profile is extended at the center by the "+
581 "corresponding radius.",true) {
583 public double getRadius(double x, double radius, double length, double param) {
588 return sqrt(2*radius*x-x*x); // radius/length * sphere
592 POWER("Power series",
593 "A power series nose cone has a profile of "+
594 "<i>Radius</i> × (<i>x</i> / <i>Length</i>)" +
595 "<sup><i>k</i></sup> "+
596 "where <i>k</i> is the shape parameter. For <i>k</i>=0.5 this is a "+
597 "<b>½-power</b> or <b>parabolic</b> nose cone, for <i>k</i>=0.75 a "+
598 "<b>¾-power</b>, and for <i>k</i>=1 a <b>conical</b> nose cone.",
599 "A power series transition has a profile of "+
600 "<i>Radius</i> × (<i>x</i> / <i>Length</i>)" +
601 "<sup><i>k</i></sup> "+
602 "where <i>k</i> is the shape parameter. For <i>k</i>=0.5 the transition is "+
603 "<b>½-power</b> or <b>parabolic</b>, for <i>k</i>=0.75 a <b>¾-power</b>, and for " +
604 "<i>k</i>=1 <b>conical</b>.",true) {
606 public boolean usesParameter() { // Range 0...1
610 public double defaultParameter() {
614 public double getRadius(double x, double radius, double length, double param) {
620 if (param<=0.00001) {
626 return radius*Math.pow(x/length, param);
631 PARABOLIC("Parabolic series",
632 "A parabolic series nose cone has a profile of a parabola. The shape "+
633 "parameter defines the segment of the parabola to utilize. The shape " +
634 "parameter 1.0 produces a <b>full parabola</b> which is tangent to the body " +
635 "tube, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a " +
636 "<b>1/2 parabola</b> and 0 produces a <b>conical</b> nose cone.",
637 "A parabolic series transition has a profile of a parabola. The shape "+
638 "parameter defines the segment of the parabola to utilize. The shape " +
639 "parameter 1.0 produces a <b>full parabola</b> which is tangent to the body " +
640 "tube at the aft end, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a " +
641 "<b>1/2 parabola</b> and 0 produces a <b>conical</b> transition.") {
643 // In principle a parabolic transition is clippable, but the difference is
647 public boolean usesParameter() { // Range 0...1
651 public double defaultParameter() {
655 public double getRadius(double x, double radius, double length, double param) {
662 return radius * ((2*x/length - param*pow2(x/length))/(2-param));
668 HAACK("Haack series",
669 "The Haack series nose cones are designed to minimize drag. The shape parameter " +
670 "0 produces an <b>LD-Haack</b> or <b>Von Karman</b> nose cone, which minimizes " +
671 "drag for fixed length and diameter, while a value of 0.333 produces an " +
672 "<b>LV-Haack</b> nose cone, which minimizes drag for fixed length and volume.",
673 "The Haack series <i>nose cones</i> are designed to minimize drag. " +
674 "These transition shapes are their equivalents, but do not necessarily produce " +
675 "optimal drag for transitions. " +
676 "The shape parameter 0 produces an <b>LD-Haack</b> or <b>Von Karman</b> shape, " +
677 "while a value of 0.333 produces an <b>LV-Haack</b> shape.",true) {
679 public boolean usesParameter() {
683 public double maxParameter() {
684 return 1.0/3.0; // Range 0...1/3
687 public double getRadius(double x, double radius, double length, double param) {
694 double theta = Math.acos(1-2*x/length);
696 return radius*sqrt((theta-sin(2*theta)/2)/Math.PI);
698 return radius*sqrt((theta-sin(2*theta)/2+param*pow3(sin(theta)))/Math.PI);
702 // POLYNOMIAL("Smooth polynomial",
703 // "A polynomial is fitted such that the nose cone profile is horizontal "+
704 // "at the aft end of the transition. The angle at the tip is defined by "+
705 // "the shape parameter.",
706 // "A polynomial is fitted such that the transition profile is horizontal "+
707 // "at the aft end of the transition. The angle at the fore end is defined "+
708 // "by the shape parameter.") {
710 // public boolean usesParameter() {
714 // public double maxParameter() {
715 // return 3.0; // Range 0...3
718 // public double defaultParameter() {
721 // public double getRadius(double x, double radius, double length, double param) {
723 // assert x <= length;
724 // assert radius >= 0;
725 // assert param >= 0;
726 // assert param <= 3;
727 // // p(x) = (k-2)x^3 + (3-2k)x^2 + k*x
729 // return radius*((((param-2)*x + (3-2*param))*x + param)*x);
734 // Privete fields of the shapes
735 private final String name;
736 private final String transitionDesc;
737 private final String noseconeDesc;
738 private final boolean canClip;
740 // Non-clippable constructor
741 Shape(String name, String noseconeDesc, String transitionDesc) {
742 this(name,noseconeDesc,transitionDesc,false);
745 // Clippable constructor
746 Shape(String name, String noseconeDesc, String transitionDesc, boolean canClip) {
748 this.canClip = canClip;
749 this.noseconeDesc = noseconeDesc;
750 this.transitionDesc = transitionDesc;
755 * Return the name of the transition shape name.
757 public String getName() {
762 * Get a description of the Transition shape.
764 public String getTransitionDescription() {
765 return transitionDesc;
769 * Get a description of the NoseCone shape.
771 public String getNoseConeDescription() {
776 * Check whether the shape differs in clipped mode. The clipping should be
777 * enabled by default if possible.
779 public boolean isClippable() {
784 * Return whether the shape uses the shape parameter. (Default false.)
786 public boolean usesParameter() {
791 * Return the minimum value of the shape parameter. (Default 0.)
793 public double minParameter() {
798 * Return the maximum value of the shape parameter. (Default 1.)
800 public double maxParameter() {
805 * Return the default value of the shape parameter. (Default 0.)
807 public double defaultParameter() {
812 * Calculate the basic radius of a transition with the given radius, length and
813 * shape parameter at the point x from the tip of the component. It is assumed
814 * that the fore radius if zero and the aft radius is <code>radius >= 0</code>.
815 * Boattails are achieved by reversing the component.
817 * @param x Position from the tip of the component.
818 * @param radius Aft end radius >= 0.
819 * @param length Length of the transition >= 0.
820 * @param param Valid shape parameter.
821 * @return The basic radius at the given position.
823 public abstract double getRadius(double x, double radius, double length, double param);
827 * Returns the name of the shape (same as getName()).
830 public String toString() {