1 package net.sf.openrocket.rocketcomponent;
3 import static java.lang.Math.sin;
4 import static net.sf.openrocket.util.MathUtil.*;
6 import java.util.Collection;
8 import net.sf.openrocket.l10n.Translator;
9 import net.sf.openrocket.preset.ComponentPreset;
10 import net.sf.openrocket.preset.ComponentPreset.Type;
11 import net.sf.openrocket.startup.Application;
12 import net.sf.openrocket.util.Coordinate;
13 import net.sf.openrocket.util.MathUtil;
16 public class Transition extends SymmetricComponent {
17 private static final Translator trans = Application.getTranslator();
18 private static final double CLIP_PRECISION = 0.0001;
22 private double shapeParameter;
23 private boolean clipped; // Not to be read - use isClipped(), which may be overriden
25 private double radius1, radius2;
26 private boolean autoRadius1, autoRadius2; // Whether the start radius is automatic
29 private double foreShoulderRadius;
30 private double foreShoulderThickness;
31 private double foreShoulderLength;
32 private boolean foreShoulderCapped;
33 private double aftShoulderRadius;
34 private double aftShoulderThickness;
35 private double aftShoulderLength;
36 private boolean aftShoulderCapped;
39 // Used to cache the clip length
40 private double clipLength = -1;
45 this.radius1 = DEFAULT_RADIUS;
46 this.radius2 = DEFAULT_RADIUS;
47 this.length = DEFAULT_RADIUS * 3;
48 this.autoRadius1 = true;
49 this.autoRadius2 = true;
51 this.type = Shape.CONICAL;
52 this.shapeParameter = 0;
56 //////// Length ////////
58 public void setLength( double length ) {
59 if ( this.length == length ) {
62 // Need to clearPreset when length changes.
64 super.setLength( length );
68 //////// Fore radius ////////
72 public double getForeRadius() {
73 if (isForeRadiusAutomatic()) {
74 // Get the automatic radius from the front
76 SymmetricComponent c = this.getPreviousSymmetricComponent();
78 r = c.getFrontAutoRadius();
87 public void setForeRadius(double radius) {
88 if ((this.radius1 == radius) && (autoRadius1 == false))
91 this.autoRadius1 = false;
92 this.radius1 = Math.max(radius, 0);
94 if (this.thickness > this.radius1 && this.thickness > this.radius2)
95 this.thickness = Math.max(this.radius1, this.radius2);
98 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
102 public boolean isForeRadiusAutomatic() {
106 public void setForeRadiusAutomatic(boolean auto) {
107 if (autoRadius1 == auto)
113 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
117 //////// Aft radius /////////
120 public double getAftRadius() {
121 if (isAftRadiusAutomatic()) {
122 // Return the auto radius from the rear
124 SymmetricComponent c = this.getNextSymmetricComponent();
126 r = c.getRearAutoRadius();
137 public void setAftRadius(double radius) {
138 if ((this.radius2 == radius) && (autoRadius2 == false))
141 this.autoRadius2 = false;
142 this.radius2 = Math.max(radius, 0);
144 if (this.thickness > this.radius1 && this.thickness > this.radius2)
145 this.thickness = Math.max(this.radius1, this.radius2);
148 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
152 public boolean isAftRadiusAutomatic() {
156 public void setAftRadiusAutomatic(boolean auto) {
157 if (autoRadius2 == auto)
163 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
168 //// Radius automatics
171 protected double getFrontAutoRadius() {
172 if (isAftRadiusAutomatic())
174 return getAftRadius();
179 protected double getRearAutoRadius() {
180 if (isForeRadiusAutomatic())
182 return getForeRadius();
188 //////// Type & shape /////////
190 public Shape getType() {
194 public void setType(Shape type) {
196 throw new IllegalArgumentException("setType called with null argument");
198 if (this.type == type)
201 this.clipped = type.isClippable();
202 this.shapeParameter = type.defaultParameter();
203 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
206 public double getShapeParameter() {
207 return shapeParameter;
210 public void setShapeParameter(double n) {
211 if (shapeParameter == n)
213 this.shapeParameter = MathUtil.clamp(n, type.minParameter(), type.maxParameter());
214 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
217 public boolean isClipped() {
218 if (!type.isClippable())
223 public void setClipped(boolean c) {
227 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
230 public boolean isClippedEnabled() {
231 return type.isClippable();
234 public double getShapeParameterMin() {
235 return type.minParameter();
238 public double getShapeParameterMax() {
239 return type.maxParameter();
243 //////// Shoulders ////////
245 public double getForeShoulderRadius() {
246 return foreShoulderRadius;
249 public void setForeShoulderRadius(double foreShoulderRadius) {
250 if (MathUtil.equals(this.foreShoulderRadius, foreShoulderRadius))
252 this.foreShoulderRadius = foreShoulderRadius;
254 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
257 public double getForeShoulderThickness() {
258 return foreShoulderThickness;
261 public void setForeShoulderThickness(double foreShoulderThickness) {
262 if (MathUtil.equals(this.foreShoulderThickness, foreShoulderThickness))
264 this.foreShoulderThickness = foreShoulderThickness;
265 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
268 public double getForeShoulderLength() {
269 return foreShoulderLength;
272 public void setForeShoulderLength(double foreShoulderLength) {
273 if (MathUtil.equals(this.foreShoulderLength, foreShoulderLength))
275 this.foreShoulderLength = foreShoulderLength;
276 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
279 public boolean isForeShoulderCapped() {
280 return foreShoulderCapped;
283 public void setForeShoulderCapped(boolean capped) {
284 if (this.foreShoulderCapped == capped)
286 this.foreShoulderCapped = capped;
287 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
293 public double getAftShoulderRadius() {
294 return aftShoulderRadius;
297 public void setAftShoulderRadius(double aftShoulderRadius) {
298 if (MathUtil.equals(this.aftShoulderRadius, aftShoulderRadius))
300 this.aftShoulderRadius = aftShoulderRadius;
302 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
305 public double getAftShoulderThickness() {
306 return aftShoulderThickness;
309 public void setAftShoulderThickness(double aftShoulderThickness) {
310 if (MathUtil.equals(this.aftShoulderThickness, aftShoulderThickness))
312 this.aftShoulderThickness = aftShoulderThickness;
313 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
316 public double getAftShoulderLength() {
317 return aftShoulderLength;
320 public void setAftShoulderLength(double aftShoulderLength) {
321 if (MathUtil.equals(this.aftShoulderLength, aftShoulderLength))
323 this.aftShoulderLength = aftShoulderLength;
324 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
327 public boolean isAftShoulderCapped() {
328 return aftShoulderCapped;
331 public void setAftShoulderCapped(boolean capped) {
332 if (this.aftShoulderCapped == capped)
334 this.aftShoulderCapped = capped;
335 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
341 /////////// Shape implementations ////////////
346 * Return the radius at point x of the transition.
349 public double getRadius(double x) {
350 if (x < 0 || x > length)
353 double r1 = getForeRadius();
354 double r2 = getAftRadius();
367 // Check clip calculation
369 calculateClip(r1, r2);
370 return type.getRadius(clipLength + x, r2, clipLength + length, shapeParameter);
373 return r1 + type.getRadius(x, r2 - r1, length, shapeParameter);
378 * Numerically solve clipLength from the equation
379 * r1 == type.getRadius(clipLength,r2,clipLength+length)
380 * using a binary search. It assumes getOuterRadius() to be monotonically increasing.
382 private void calculateClip(double r1, double r2) {
383 double min = 0, max = length;
402 // getR(min,min+length,r2) - r1 < 0
403 // getR(max,max+length,r2) - r1 > 0
406 while (type.getRadius(max, r2, max + length, shapeParameter) - r1 < 0) {
415 clipLength = (min + max) / 2;
416 if ((max - min) < CLIP_PRECISION)
418 double val = type.getRadius(clipLength, r2, clipLength + length, shapeParameter);
429 public double getInnerRadius(double x) {
430 return Math.max(getRadius(x) - thickness, 0);
436 public Collection<Coordinate> getComponentBounds() {
437 Collection<Coordinate> bounds = super.getComponentBounds();
438 if (foreShoulderLength > 0.001)
439 addBound(bounds, -foreShoulderLength, foreShoulderRadius);
440 if (aftShoulderLength > 0.001)
441 addBound(bounds, getLength() + aftShoulderLength, aftShoulderRadius);
446 public double getComponentVolume() {
447 double volume = super.getComponentVolume();
448 if (getForeShoulderLength() > 0.001) {
449 final double or = getForeShoulderRadius();
450 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
451 volume += ringVolume( or, ir, getForeShoulderLength() );
453 if (isForeShoulderCapped()) {
454 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
455 volume += ringVolume(ir, 0, getForeShoulderThickness() );
458 if (getAftShoulderLength() > 0.001) {
459 final double or = getAftShoulderRadius();
460 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
461 volume += ringVolume(or, ir, getAftShoulderLength() );
463 if (isAftShoulderCapped()) {
464 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
465 volume += ringVolume(ir, 0, getAftShoulderThickness() );
472 public Coordinate getComponentCG() {
473 Coordinate cg = super.getComponentCG();
474 if (getForeShoulderLength() > 0.001) {
475 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
476 cg = cg.average(ringCG(getForeShoulderRadius(), ir, -getForeShoulderLength(), 0,
477 getMaterial().getDensity()));
479 if (isForeShoulderCapped()) {
480 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
481 cg = cg.average(ringCG(ir, 0, -getForeShoulderLength(),
482 getForeShoulderThickness() - getForeShoulderLength(),
483 getMaterial().getDensity()));
486 if (getAftShoulderLength() > 0.001) {
487 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
488 cg = cg.average(ringCG(getAftShoulderRadius(), ir, getLength(),
489 getLength() + getAftShoulderLength(), getMaterial().getDensity()));
491 if (isAftShoulderCapped()) {
492 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
493 cg = cg.average(ringCG(ir, 0,
494 getLength() + getAftShoulderLength() - getAftShoulderThickness(),
495 getLength() + getAftShoulderLength(), getMaterial().getDensity()));
502 * The moments of inertia are not explicitly corrected for the shoulders.
503 * However, since the mass is corrected, the inertia is automatically corrected
504 * to very nearly the correct value.
510 * Returns the name of the component ("Transition").
513 public String getComponentName() {
515 return trans.get("Transition.Transition");
519 protected void componentChanged(ComponentChangeEvent e) {
520 super.componentChanged(e);
525 * Check whether the given type can be added to this component. Transitions allow any
526 * InternalComponents to be added.
528 * @param ctype The RocketComponent class type to add.
529 * @return Whether such a component can be added.
532 public boolean isCompatible(Class<? extends RocketComponent> ctype) {
533 if (InternalComponent.class.isAssignableFrom(ctype))
539 public Type getPresetType() {
540 return ComponentPreset.Type.TRANSITION;
545 protected void loadFromPreset(ComponentPreset preset) {
547 if ( preset.has(ComponentPreset.SHAPE) ) {
548 Shape s = preset.get(ComponentPreset.SHAPE);
551 if ( preset.has(ComponentPreset.AFT_OUTER_DIAMETER) ) {
552 double outerDiameter = preset.get(ComponentPreset.AFT_OUTER_DIAMETER);
553 this.setAftRadiusAutomatic(false);
554 this.setAftRadius(outerDiameter/2.0);
556 if ( preset.has(ComponentPreset.AFT_SHOULDER_LENGTH) ) {
557 double d = preset.get(ComponentPreset.AFT_SHOULDER_LENGTH);
558 this.setAftShoulderLength(d);
560 if ( preset.has(ComponentPreset.AFT_SHOULDER_DIAMETER) ) {
561 double d = preset.get(ComponentPreset.AFT_SHOULDER_DIAMETER);
562 this.setAftShoulderRadius(d/2.0);
564 if ( preset.has(ComponentPreset.FORE_OUTER_DIAMETER) ) {
565 double outerDiameter = preset.get(ComponentPreset.FORE_OUTER_DIAMETER);
566 this.setForeRadiusAutomatic(false);
567 this.setForeRadius(outerDiameter/2.0);
569 if ( preset.has(ComponentPreset.FORE_SHOULDER_LENGTH) ) {
570 double d = preset.get(ComponentPreset.FORE_SHOULDER_LENGTH);
571 this.setForeShoulderLength(d);
573 if ( preset.has(ComponentPreset.FORE_SHOULDER_DIAMETER) ) {
574 double d = preset.get(ComponentPreset.FORE_SHOULDER_DIAMETER);
575 this.setForeShoulderRadius(d/2.0);
578 super.loadFromPreset(preset);
580 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
585 * An enumeration listing the possible shapes of transitions.
587 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
589 public static enum Shape {
595 CONICAL(trans.get("Shape.Conical"),
596 //// A conical nose cone has a profile of a triangle.
597 trans.get("Shape.Conical.desc1"),
598 //// A conical transition has straight sides.
599 trans.get("Shape.Conical.desc2")) {
601 public double getRadius(double x, double radius, double length, double param) {
605 return radius * x / length;
610 * Ogive shape. The shape parameter is the portion of an extended tangent ogive
611 * that will be used. That is, for param==1 a tangent ogive will be produced, and
612 * for smaller values the shape straightens out into a cone at param==0.
615 OGIVE(trans.get("Shape.Ogive"),
616 //// An ogive nose cone has a profile that is a segment of a circle. The shape parameter value 1 produces a <b>tangent ogive</b>, which has a smooth transition to the body tube, values less than 1 produce <b>secant ogives</b>.
617 trans.get("Shape.Ogive.desc1"),
618 //// An ogive transition has a profile that is a segment of a circle. The shape parameter value 1 produces a <b>tangent ogive</b>, which has a smooth transition to the body tube at the aft end, values less than 1 produce <b>secant ogives</b>.
619 trans.get("Shape.Ogive.desc2")) {
621 public boolean usesParameter() {
622 return true; // Range 0...1 is default
626 public double defaultParameter() {
627 return 1.0; // Tangent ogive by default
631 public double getRadius(double x, double radius, double length, double param) {
638 // Impossible to calculate ogive for length < radius, scale instead
639 // TODO: LOW: secant ogive could be calculated lower
640 if (length < radius) {
641 x = x * radius / length;
646 return CONICAL.getRadius(x, radius, length, param);
648 // Radius of circle is:
649 double R = MathUtil.safeSqrt((pow2(length) + pow2(radius)) *
650 (pow2((2 - param) * length) + pow2(param * radius)) / (4 * pow2(param * radius)));
651 double L = length / param;
652 // double R = (radius + length*length/(radius*param*param))/2;
653 double y0 = MathUtil.safeSqrt(R * R - L * L);
654 return MathUtil.safeSqrt(R * R - (L - x) * (L - x)) - y0;
662 ELLIPSOID(trans.get("Shape.Ellipsoid"),
663 //// An ellipsoidal nose cone has a profile of a half-ellipse with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>.
664 trans.get("Shape.Ellipsoid.desc1"),
665 //// An ellipsoidal transition has a profile of a half-ellipse with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>. If the transition is not clipped, then the profile is extended at the center by the corresponding radius.
666 trans.get("Shape.Ellipsoid.desc2"), true) {
668 public double getRadius(double x, double radius, double length, double param) {
672 x = x * radius / length;
673 return MathUtil.safeSqrt(2 * radius * x - x * x); // radius/length * sphere
678 POWER(trans.get("Shape.Powerseries"),
679 trans.get("Shape.Powerseries.desc1"),
680 trans.get("Shape.Powerseries.desc2"), true) {
682 public boolean usesParameter() { // Range 0...1
687 public double defaultParameter() {
692 public double getRadius(double x, double radius, double length, double param) {
698 if (param <= 0.00001) {
704 return radius * Math.pow(x / length, param);
709 //// Parabolic series
710 PARABOLIC(trans.get("Shape.Parabolicseries"),
711 ////A parabolic series nose cone has a profile of a parabola. The shape parameter defines the segment of the parabola to utilize. The shape parameter 1.0 produces a <b>full parabola</b> which is tangent to the body tube, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a <b>1/2 parabola</b> and 0 produces a <b>conical</b> nose cone.
712 trans.get("Shape.Parabolicseries.desc1"),
713 ////A parabolic series transition has a profile of a parabola. The shape parameter defines the segment of the parabola to utilize. The shape parameter 1.0 produces a <b>full parabola</b> which is tangent to the body tube at the aft end, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a <b>1/2 parabola</b> and 0 produces a <b>conical</b> transition.
714 trans.get("Shape.Parabolicseries.desc2")) {
716 // In principle a parabolic transition is clippable, but the difference is
720 public boolean usesParameter() { // Range 0...1
725 public double defaultParameter() {
730 public double getRadius(double x, double radius, double length, double param) {
737 return radius * ((2 * x / length - param * pow2(x / length)) / (2 - param));
742 HAACK(trans.get("Shape.Haackseries"),
743 //// The Haack series nose cones are designed to minimize drag. The shape parameter 0 produces an <b>LD-Haack</b> or <b>Von Karman</b> nose cone, which minimizes drag for fixed length and diameter, while a value of 0.333 produces an <b>LV-Haack</b> nose cone, which minimizes drag for fixed length and volume.
744 trans.get("Shape.Haackseries.desc1"),
745 //// The Haack series <i>nose cones</i> are designed to minimize drag. These transition shapes are their equivalents, but do not necessarily produce optimal drag for transitions. The shape parameter 0 produces an <b>LD-Haack</b> or <b>Von Karman</b> shape, while a value of 0.333 produces an <b>LV-Haack</b> shape.
746 trans.get("Shape.Haackseries.desc2"), true) {
749 public boolean usesParameter() {
754 public double maxParameter() {
755 return 1.0 / 3.0; // Range 0...1/3
759 public double getRadius(double x, double radius, double length, double param) {
766 double theta = Math.acos(1 - 2 * x / length);
767 if (MathUtil.equals(param, 0)) {
768 return radius * MathUtil.safeSqrt((theta - sin(2 * theta) / 2) / Math.PI);
770 return radius * MathUtil.safeSqrt((theta - sin(2 * theta) / 2 + param * pow3(sin(theta))) / Math.PI);
774 // POLYNOMIAL("Smooth polynomial",
775 // "A polynomial is fitted such that the nose cone profile is horizontal "+
776 // "at the aft end of the transition. The angle at the tip is defined by "+
777 // "the shape parameter.",
778 // "A polynomial is fitted such that the transition profile is horizontal "+
779 // "at the aft end of the transition. The angle at the fore end is defined "+
780 // "by the shape parameter.") {
782 // public boolean usesParameter() {
786 // public double maxParameter() {
787 // return 3.0; // Range 0...3
790 // public double defaultParameter() {
793 // public double getRadius(double x, double radius, double length, double param) {
795 // assert x <= length;
796 // assert radius >= 0;
797 // assert param >= 0;
798 // assert param <= 3;
799 // // p(x) = (k-2)x^3 + (3-2k)x^2 + k*x
801 // return radius*((((param-2)*x + (3-2*param))*x + param)*x);
806 // Privete fields of the shapes
807 private final String name;
808 private final String transitionDesc;
809 private final String noseconeDesc;
810 private final boolean canClip;
812 // Non-clippable constructor
813 Shape(String name, String noseconeDesc, String transitionDesc) {
814 this(name, noseconeDesc, transitionDesc, false);
817 // Clippable constructor
818 Shape(String name, String noseconeDesc, String transitionDesc, boolean canClip) {
820 this.canClip = canClip;
821 this.noseconeDesc = noseconeDesc;
822 this.transitionDesc = transitionDesc;
827 * Return the name of the transition shape name.
829 public String getName() {
834 * Get a description of the Transition shape.
836 public String getTransitionDescription() {
837 return transitionDesc;
841 * Get a description of the NoseCone shape.
843 public String getNoseConeDescription() {
848 * Check whether the shape differs in clipped mode. The clipping should be
849 * enabled by default if possible.
851 public boolean isClippable() {
856 * Return whether the shape uses the shape parameter. (Default false.)
858 public boolean usesParameter() {
863 * Return the minimum value of the shape parameter. (Default 0.)
865 public double minParameter() {
870 * Return the maximum value of the shape parameter. (Default 1.)
872 public double maxParameter() {
877 * Return the default value of the shape parameter. (Default 0.)
879 public double defaultParameter() {
884 * Calculate the basic radius of a transition with the given radius, length and
885 * shape parameter at the point x from the tip of the component. It is assumed
886 * that the fore radius if zero and the aft radius is <code>radius >= 0</code>.
887 * Boattails are achieved by reversing the component.
889 * @param x Position from the tip of the component.
890 * @param radius Aft end radius >= 0.
891 * @param length Length of the transition >= 0.
892 * @param param Valid shape parameter.
893 * @return The basic radius at the given position.
895 public abstract double getRadius(double x, double radius, double length, double param);
899 * Returns the name of the shape (same as getName()).
902 public String toString() {