1 package net.sf.openrocket.rocketcomponent;
3 import static java.lang.Math.*;
4 import static net.sf.openrocket.util.Chars.*;
5 import static net.sf.openrocket.util.MathUtil.*;
7 import java.util.Collection;
9 import net.sf.openrocket.util.Coordinate;
10 import net.sf.openrocket.util.MathUtil;
13 public class Transition extends SymmetricComponent {
14 private static final double CLIP_PRECISION = 0.0001;
18 private double shapeParameter;
19 private boolean clipped; // Not to be read - use isClipped(), which may be overriden
21 private double radius1, radius2;
22 private boolean autoRadius1, autoRadius2; // Whether the start radius is automatic
25 private double foreShoulderRadius;
26 private double foreShoulderThickness;
27 private double foreShoulderLength;
28 private boolean foreShoulderCapped;
29 private double aftShoulderRadius;
30 private double aftShoulderThickness;
31 private double aftShoulderLength;
32 private boolean aftShoulderCapped;
35 // Used to cache the clip length
36 private double clipLength=-1;
41 this.radius1 = DEFAULT_RADIUS;
42 this.radius2 = DEFAULT_RADIUS;
43 this.length = DEFAULT_RADIUS * 3;
44 this.autoRadius1 = true;
45 this.autoRadius2 = true;
47 this.type = Shape.CONICAL;
48 this.shapeParameter = 0;
55 //////// Fore radius ////////
59 public double getForeRadius() {
60 if (isForeRadiusAutomatic()) {
61 // Get the automatic radius from the front
63 SymmetricComponent c = this.getPreviousSymmetricComponent();
65 r = c.getFrontAutoRadius();
74 public void setForeRadius(double radius) {
75 if ((this.radius1 == radius) && (autoRadius1 == false))
78 this.autoRadius1 = false;
79 this.radius1 = Math.max(radius,0);
81 if (this.thickness > this.radius1 && this.thickness > this.radius2)
82 this.thickness = Math.max(this.radius1, this.radius2);
83 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
87 public boolean isForeRadiusAutomatic() {
91 public void setForeRadiusAutomatic(boolean auto) {
92 if (autoRadius1 == auto)
96 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
100 //////// Aft radius /////////
103 public double getAftRadius() {
104 if (isAftRadiusAutomatic()) {
105 // Return the auto radius from the rear
107 SymmetricComponent c = this.getNextSymmetricComponent();
109 r = c.getRearAutoRadius();
120 public void setAftRadius(double radius) {
121 if ((this.radius2 == radius) && (autoRadius2 == false))
124 this.autoRadius2 = false;
125 this.radius2 = Math.max(radius,0);
127 if (this.thickness > this.radius1 && this.thickness > this.radius2)
128 this.thickness = Math.max(this.radius1, this.radius2);
129 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
133 public boolean isAftRadiusAutomatic() {
137 public void setAftRadiusAutomatic(boolean auto) {
138 if (autoRadius2 == auto)
142 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
147 //// Radius automatics
150 protected double getFrontAutoRadius() {
151 if (isAftRadiusAutomatic())
153 return getAftRadius();
158 protected double getRearAutoRadius() {
159 if (isForeRadiusAutomatic())
161 return getForeRadius();
167 //////// Type & shape /////////
169 public Shape getType() {
173 public void setType(Shape type) {
175 throw new IllegalArgumentException("BUG: setType called with null argument");
177 if (this.type == type)
180 this.clipped = type.isClippable();
181 this.shapeParameter = type.defaultParameter();
182 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
185 public double getShapeParameter() {
186 return shapeParameter;
189 public void setShapeParameter(double n) {
190 if (shapeParameter == n)
192 this.shapeParameter = MathUtil.clamp(n, type.minParameter(), type.maxParameter());
193 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
196 public boolean isClipped() {
197 if (!type.isClippable())
202 public void setClipped(boolean c) {
206 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
209 public boolean isClippedEnabled() {
210 return type.isClippable();
213 public double getShapeParameterMin() {
214 return type.minParameter();
217 public double getShapeParameterMax() {
218 return type.maxParameter();
222 //////// Shoulders ////////
224 public double getForeShoulderRadius() {
225 return foreShoulderRadius;
228 public void setForeShoulderRadius(double foreShoulderRadius) {
229 if (MathUtil.equals(this.foreShoulderRadius, foreShoulderRadius))
231 this.foreShoulderRadius = foreShoulderRadius;
232 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
235 public double getForeShoulderThickness() {
236 return foreShoulderThickness;
239 public void setForeShoulderThickness(double foreShoulderThickness) {
240 if (MathUtil.equals(this.foreShoulderThickness, foreShoulderThickness))
242 this.foreShoulderThickness = foreShoulderThickness;
243 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
246 public double getForeShoulderLength() {
247 return foreShoulderLength;
250 public void setForeShoulderLength(double foreShoulderLength) {
251 if (MathUtil.equals(this.foreShoulderLength, foreShoulderLength))
253 this.foreShoulderLength = foreShoulderLength;
254 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
257 public boolean isForeShoulderCapped() {
258 return foreShoulderCapped;
261 public void setForeShoulderCapped(boolean capped) {
262 if (this.foreShoulderCapped == capped)
264 this.foreShoulderCapped = capped;
265 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
271 public double getAftShoulderRadius() {
272 return aftShoulderRadius;
275 public void setAftShoulderRadius(double aftShoulderRadius) {
276 if (MathUtil.equals(this.aftShoulderRadius, aftShoulderRadius))
278 this.aftShoulderRadius = aftShoulderRadius;
279 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
282 public double getAftShoulderThickness() {
283 return aftShoulderThickness;
286 public void setAftShoulderThickness(double aftShoulderThickness) {
287 if (MathUtil.equals(this.aftShoulderThickness, aftShoulderThickness))
289 this.aftShoulderThickness = aftShoulderThickness;
290 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
293 public double getAftShoulderLength() {
294 return aftShoulderLength;
297 public void setAftShoulderLength(double aftShoulderLength) {
298 if (MathUtil.equals(this.aftShoulderLength, aftShoulderLength))
300 this.aftShoulderLength = aftShoulderLength;
301 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
304 public boolean isAftShoulderCapped() {
305 return aftShoulderCapped;
308 public void setAftShoulderCapped(boolean capped) {
309 if (this.aftShoulderCapped == capped)
311 this.aftShoulderCapped = capped;
312 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
318 /////////// Shape implementations ////////////
323 * Return the radius at point x of the transition.
326 public double getRadius(double x) {
330 double r1=getForeRadius();
331 double r2=getAftRadius();
344 // Check clip calculation
346 calculateClip(r1,r2);
347 return type.getRadius(clipLength+x, r2, clipLength+length, shapeParameter);
350 return r1 + type.getRadius(x, r2-r1, length, shapeParameter);
355 * Numerically solve clipLength from the equation
356 * r1 == type.getRadius(clipLength,r2,clipLength+length)
357 * using a binary search. It assumes getRadius() to be monotonically increasing.
359 private void calculateClip(double r1, double r2) {
360 double min=0, max=length;
379 // getR(min,min+length,r2) - r1 < 0
380 // getR(max,max+length,r2) - r1 > 0
383 while (type.getRadius(max, r2, max+length, shapeParameter) - r1 < 0) {
392 clipLength = (min+max)/2;
393 if ((max-min)<CLIP_PRECISION)
395 double val = type.getRadius(clipLength, r2, clipLength+length, shapeParameter);
406 public double getInnerRadius(double x) {
407 return Math.max(getRadius(x)-thickness,0);
413 public Collection<Coordinate> getComponentBounds() {
414 Collection<Coordinate> bounds = super.getComponentBounds();
415 if (foreShoulderLength > 0.001)
416 addBound(bounds, -foreShoulderLength, foreShoulderRadius);
417 if (aftShoulderLength > 0.001)
418 addBound(bounds, getLength() + aftShoulderLength, aftShoulderRadius);
423 public double getComponentMass() {
424 double mass = super.getComponentMass();
425 if (getForeShoulderLength() > 0.001) {
426 final double or = getForeShoulderRadius();
427 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
428 mass += ringMass(or, ir, getForeShoulderLength(), getMaterial().getDensity());
430 if (isForeShoulderCapped()) {
431 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
432 mass += ringMass(ir, 0, getForeShoulderThickness(), getMaterial().getDensity());
435 if (getAftShoulderLength() > 0.001) {
436 final double or = getAftShoulderRadius();
437 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
438 mass += ringMass(or, ir, getAftShoulderLength(), getMaterial().getDensity());
440 if (isAftShoulderCapped()) {
441 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
442 mass += ringMass(ir, 0, getAftShoulderThickness(), getMaterial().getDensity());
449 public Coordinate getComponentCG() {
450 Coordinate cg = super.getComponentCG();
451 if (getForeShoulderLength() > 0.001) {
452 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
453 cg = cg.average(ringCG(getForeShoulderRadius(), ir, -getForeShoulderLength(), 0,
454 getMaterial().getDensity()));
456 if (isForeShoulderCapped()) {
457 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
458 cg = cg.average(ringCG(ir, 0, -getForeShoulderLength(),
459 getForeShoulderThickness()-getForeShoulderLength(),
460 getMaterial().getDensity()));
463 if (getAftShoulderLength() > 0.001) {
464 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
465 cg = cg.average(ringCG(getAftShoulderRadius(), ir, getLength(),
466 getLength()+getAftShoulderLength(), getMaterial().getDensity()));
468 if (isAftShoulderCapped()) {
469 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
470 cg = cg.average(ringCG(ir, 0,
471 getLength()+getAftShoulderLength()-getAftShoulderThickness(),
472 getLength()+getAftShoulderLength(), getMaterial().getDensity()));
479 * The moments of inertia are not explicitly corrected for the shoulders.
480 * However, since the mass is corrected, the inertia is automatically corrected
481 * to very nearly the correct value.
487 * Returns the name of the component ("Transition").
490 public String getComponentName() {
495 protected void componentChanged(ComponentChangeEvent e) {
496 super.componentChanged(e);
503 * An enumeration listing the possible shapes of transitions.
505 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
507 public static enum Shape {
513 "A conical nose cone has a profile of a triangle.",
514 "A conical transition has straight sides.") {
516 public double getRadius(double x, double radius, double length, double param) {
520 return radius*x/length;
525 * Ogive shape. The shape parameter is the portion of an extended tangent ogive
526 * that will be used. That is, for param==1 a tangent ogive will be produced, and
527 * for smaller values the shape straightens out into a cone at param==0.
530 "An ogive nose cone has a profile that is a segment of a circle. " +
531 "The shape parameter value 1 produces a <b>tangent ogive</b>, which has " +
532 "a smooth transition to the body tube, values less than 1 produce "+
533 "<b>secant ogives</b>.",
534 "An ogive transition has a profile that is a segment of a circle. " +
535 "The shape parameter value 1 produces a <b>tangent ogive</b>, which has " +
536 "a smooth transition to the body tube at the aft end, values less than 1 " +
537 "produce <b>secant ogives</b>.") {
539 public boolean usesParameter() {
540 return true; // Range 0...1 is default
543 public double defaultParameter() {
544 return 1.0; // Tangent ogive by default
547 public double getRadius(double x, double radius, double length, double param) {
554 // Impossible to calculate ogive for length < radius, scale instead
555 // TODO: LOW: secant ogive could be calculated lower
556 if (length < radius) {
557 x = x * radius / length;
562 return CONICAL.getRadius(x, radius, length, param);
564 // Radius of circle is:
565 double R = sqrt((pow2(length)+pow2(radius)) *
566 (pow2((2-param)*length) + pow2(param*radius))/(4*pow2(param*radius)));
567 double L = length/param;
568 // double R = (radius + length*length/(radius*param*param))/2;
569 double y0 = sqrt(R*R - L*L);
570 return sqrt(R*R - (L-x)*(L-x)) - y0;
577 ELLIPSOID("Ellipsoid",
578 "An ellipsoidal nose cone has a profile of a half-ellipse "+
579 "with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>.",
580 "An ellipsoidal transition has a profile of a half-ellipse "+
581 "with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>. If the "+
582 "transition is not clipped, then the profile is extended at the center by the "+
583 "corresponding radius.",true) {
585 public double getRadius(double x, double radius, double length, double param) {
590 return sqrt(2*radius*x-x*x); // radius/length * sphere
594 POWER("Power series",
595 "A power series nose cone has a profile of "+
596 "<i>Radius</i> × (<i>x</i> / <i>Length</i>)" +
597 "<sup><i>k</i></sup> "+
598 "where <i>k</i> is the shape parameter. For <i>k</i>=0.5 this is a "+
599 "<b>" + FRAC12 +"-power</b> or <b>parabolic</b> nose cone, for <i>k</i>=0.75 a "+
600 "<b>" + FRAC34 +"-power</b>, and for <i>k</i>=1 a <b>conical</b> nose cone.",
601 "A power series transition has a profile of "+
602 "<i>Radius</i> × (<i>x</i> / <i>Length</i>)" +
603 "<sup><i>k</i></sup> "+
604 "where <i>k</i> is the shape parameter. For <i>k</i>=0.5 the transition is "+
605 "<b>" + FRAC12 + "-power</b> or <b>parabolic</b>, for <i>k</i>=0.75 a " +
606 "<b>" + FRAC34 + "-power</b>, and for <i>k</i>=1 <b>conical</b>.",true) {
608 public boolean usesParameter() { // Range 0...1
612 public double defaultParameter() {
616 public double getRadius(double x, double radius, double length, double param) {
622 if (param<=0.00001) {
628 return radius*Math.pow(x/length, param);
633 PARABOLIC("Parabolic series",
634 "A parabolic series nose cone has a profile of a parabola. The shape "+
635 "parameter defines the segment of the parabola to utilize. The shape " +
636 "parameter 1.0 produces a <b>full parabola</b> which is tangent to the body " +
637 "tube, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a " +
638 "<b>1/2 parabola</b> and 0 produces a <b>conical</b> nose cone.",
639 "A parabolic series transition has a profile of a parabola. The shape "+
640 "parameter defines the segment of the parabola to utilize. The shape " +
641 "parameter 1.0 produces a <b>full parabola</b> which is tangent to the body " +
642 "tube at the aft end, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a " +
643 "<b>1/2 parabola</b> and 0 produces a <b>conical</b> transition.") {
645 // In principle a parabolic transition is clippable, but the difference is
649 public boolean usesParameter() { // Range 0...1
653 public double defaultParameter() {
657 public double getRadius(double x, double radius, double length, double param) {
664 return radius * ((2*x/length - param*pow2(x/length))/(2-param));
670 HAACK("Haack series",
671 "The Haack series nose cones are designed to minimize drag. The shape parameter " +
672 "0 produces an <b>LD-Haack</b> or <b>Von Karman</b> nose cone, which minimizes " +
673 "drag for fixed length and diameter, while a value of 0.333 produces an " +
674 "<b>LV-Haack</b> nose cone, which minimizes drag for fixed length and volume.",
675 "The Haack series <i>nose cones</i> are designed to minimize drag. " +
676 "These transition shapes are their equivalents, but do not necessarily produce " +
677 "optimal drag for transitions. " +
678 "The shape parameter 0 produces an <b>LD-Haack</b> or <b>Von Karman</b> shape, " +
679 "while a value of 0.333 produces an <b>LV-Haack</b> shape.",true) {
681 public boolean usesParameter() {
685 public double maxParameter() {
686 return 1.0/3.0; // Range 0...1/3
689 public double getRadius(double x, double radius, double length, double param) {
696 double theta = Math.acos(1-2*x/length);
698 return radius*sqrt((theta-sin(2*theta)/2)/Math.PI);
700 return radius*sqrt((theta-sin(2*theta)/2+param*pow3(sin(theta)))/Math.PI);
704 // POLYNOMIAL("Smooth polynomial",
705 // "A polynomial is fitted such that the nose cone profile is horizontal "+
706 // "at the aft end of the transition. The angle at the tip is defined by "+
707 // "the shape parameter.",
708 // "A polynomial is fitted such that the transition profile is horizontal "+
709 // "at the aft end of the transition. The angle at the fore end is defined "+
710 // "by the shape parameter.") {
712 // public boolean usesParameter() {
716 // public double maxParameter() {
717 // return 3.0; // Range 0...3
720 // public double defaultParameter() {
723 // public double getRadius(double x, double radius, double length, double param) {
725 // assert x <= length;
726 // assert radius >= 0;
727 // assert param >= 0;
728 // assert param <= 3;
729 // // p(x) = (k-2)x^3 + (3-2k)x^2 + k*x
731 // return radius*((((param-2)*x + (3-2*param))*x + param)*x);
736 // Privete fields of the shapes
737 private final String name;
738 private final String transitionDesc;
739 private final String noseconeDesc;
740 private final boolean canClip;
742 // Non-clippable constructor
743 Shape(String name, String noseconeDesc, String transitionDesc) {
744 this(name,noseconeDesc,transitionDesc,false);
747 // Clippable constructor
748 Shape(String name, String noseconeDesc, String transitionDesc, boolean canClip) {
750 this.canClip = canClip;
751 this.noseconeDesc = noseconeDesc;
752 this.transitionDesc = transitionDesc;
757 * Return the name of the transition shape name.
759 public String getName() {
764 * Get a description of the Transition shape.
766 public String getTransitionDescription() {
767 return transitionDesc;
771 * Get a description of the NoseCone shape.
773 public String getNoseConeDescription() {
778 * Check whether the shape differs in clipped mode. The clipping should be
779 * enabled by default if possible.
781 public boolean isClippable() {
786 * Return whether the shape uses the shape parameter. (Default false.)
788 public boolean usesParameter() {
793 * Return the minimum value of the shape parameter. (Default 0.)
795 public double minParameter() {
800 * Return the maximum value of the shape parameter. (Default 1.)
802 public double maxParameter() {
807 * Return the default value of the shape parameter. (Default 0.)
809 public double defaultParameter() {
814 * Calculate the basic radius of a transition with the given radius, length and
815 * shape parameter at the point x from the tip of the component. It is assumed
816 * that the fore radius if zero and the aft radius is <code>radius >= 0</code>.
817 * Boattails are achieved by reversing the component.
819 * @param x Position from the tip of the component.
820 * @param radius Aft end radius >= 0.
821 * @param length Length of the transition >= 0.
822 * @param param Valid shape parameter.
823 * @return The basic radius at the given position.
825 public abstract double getRadius(double x, double radius, double length, double param);
829 * Returns the name of the shape (same as getName()).
832 public String toString() {