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 boolean presetFilled = false;
548 if ( preset.has(ComponentPreset.FILLED ) ) {
549 presetFilled = preset.get( ComponentPreset.FILLED);
552 if ( preset.has(ComponentPreset.SHAPE) ) {
553 Shape s = preset.get(ComponentPreset.SHAPE);
556 if ( preset.has(ComponentPreset.AFT_OUTER_DIAMETER) ) {
557 double outerDiameter = preset.get(ComponentPreset.AFT_OUTER_DIAMETER);
558 this.setAftRadiusAutomatic(false);
559 this.setAftRadius(outerDiameter/2.0);
561 if ( preset.has(ComponentPreset.AFT_SHOULDER_LENGTH) ) {
562 double d = preset.get(ComponentPreset.AFT_SHOULDER_LENGTH);
563 this.setAftShoulderLength(d);
565 if ( preset.has(ComponentPreset.AFT_SHOULDER_DIAMETER) ) {
566 double d = preset.get(ComponentPreset.AFT_SHOULDER_DIAMETER);
567 this.setAftShoulderRadius(d/2.0);
568 if ( presetFilled ) {
569 this.setAftShoulderThickness(d/2.0);
572 if ( preset.has(ComponentPreset.FORE_OUTER_DIAMETER) ) {
573 double outerDiameter = preset.get(ComponentPreset.FORE_OUTER_DIAMETER);
574 this.setForeRadiusAutomatic(false);
575 this.setForeRadius(outerDiameter/2.0);
577 if ( preset.has(ComponentPreset.FORE_SHOULDER_LENGTH) ) {
578 double d = preset.get(ComponentPreset.FORE_SHOULDER_LENGTH);
579 this.setForeShoulderLength(d);
581 if ( preset.has(ComponentPreset.FORE_SHOULDER_DIAMETER) ) {
582 double d = preset.get(ComponentPreset.FORE_SHOULDER_DIAMETER);
583 this.setForeShoulderRadius(d/2.0);
584 if ( presetFilled ) {
585 this.setForeShoulderThickness(d/2.0);
589 super.loadFromPreset(preset);
591 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
596 * An enumeration listing the possible shapes of transitions.
598 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
600 public static enum Shape {
606 CONICAL(trans.get("Shape.Conical"),
607 //// A conical nose cone has a profile of a triangle.
608 trans.get("Shape.Conical.desc1"),
609 //// A conical transition has straight sides.
610 trans.get("Shape.Conical.desc2")) {
612 public double getRadius(double x, double radius, double length, double param) {
616 return radius * x / length;
621 * Ogive shape. The shape parameter is the portion of an extended tangent ogive
622 * that will be used. That is, for param==1 a tangent ogive will be produced, and
623 * for smaller values the shape straightens out into a cone at param==0.
626 OGIVE(trans.get("Shape.Ogive"),
627 //// 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>.
628 trans.get("Shape.Ogive.desc1"),
629 //// 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>.
630 trans.get("Shape.Ogive.desc2")) {
632 public boolean usesParameter() {
633 return true; // Range 0...1 is default
637 public double defaultParameter() {
638 return 1.0; // Tangent ogive by default
642 public double getRadius(double x, double radius, double length, double param) {
649 // Impossible to calculate ogive for length < radius, scale instead
650 // TODO: LOW: secant ogive could be calculated lower
651 if (length < radius) {
652 x = x * radius / length;
657 return CONICAL.getRadius(x, radius, length, param);
659 // Radius of circle is:
660 double R = MathUtil.safeSqrt((pow2(length) + pow2(radius)) *
661 (pow2((2 - param) * length) + pow2(param * radius)) / (4 * pow2(param * radius)));
662 double L = length / param;
663 // double R = (radius + length*length/(radius*param*param))/2;
664 double y0 = MathUtil.safeSqrt(R * R - L * L);
665 return MathUtil.safeSqrt(R * R - (L - x) * (L - x)) - y0;
673 ELLIPSOID(trans.get("Shape.Ellipsoid"),
674 //// An ellipsoidal nose cone has a profile of a half-ellipse with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>.
675 trans.get("Shape.Ellipsoid.desc1"),
676 //// 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.
677 trans.get("Shape.Ellipsoid.desc2"), true) {
679 public double getRadius(double x, double radius, double length, double param) {
683 x = x * radius / length;
684 return MathUtil.safeSqrt(2 * radius * x - x * x); // radius/length * sphere
689 POWER(trans.get("Shape.Powerseries"),
690 trans.get("Shape.Powerseries.desc1"),
691 trans.get("Shape.Powerseries.desc2"), true) {
693 public boolean usesParameter() { // Range 0...1
698 public double defaultParameter() {
703 public double getRadius(double x, double radius, double length, double param) {
709 if (param <= 0.00001) {
715 return radius * Math.pow(x / length, param);
720 //// Parabolic series
721 PARABOLIC(trans.get("Shape.Parabolicseries"),
722 ////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.
723 trans.get("Shape.Parabolicseries.desc1"),
724 ////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.
725 trans.get("Shape.Parabolicseries.desc2")) {
727 // In principle a parabolic transition is clippable, but the difference is
731 public boolean usesParameter() { // Range 0...1
736 public double defaultParameter() {
741 public double getRadius(double x, double radius, double length, double param) {
748 return radius * ((2 * x / length - param * pow2(x / length)) / (2 - param));
753 HAACK(trans.get("Shape.Haackseries"),
754 //// 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.
755 trans.get("Shape.Haackseries.desc1"),
756 //// 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.
757 trans.get("Shape.Haackseries.desc2"), true) {
760 public boolean usesParameter() {
765 public double maxParameter() {
766 return 1.0 / 3.0; // Range 0...1/3
770 public double getRadius(double x, double radius, double length, double param) {
777 double theta = Math.acos(1 - 2 * x / length);
778 if (MathUtil.equals(param, 0)) {
779 return radius * MathUtil.safeSqrt((theta - sin(2 * theta) / 2) / Math.PI);
781 return radius * MathUtil.safeSqrt((theta - sin(2 * theta) / 2 + param * pow3(sin(theta))) / Math.PI);
785 // POLYNOMIAL("Smooth polynomial",
786 // "A polynomial is fitted such that the nose cone profile is horizontal "+
787 // "at the aft end of the transition. The angle at the tip is defined by "+
788 // "the shape parameter.",
789 // "A polynomial is fitted such that the transition profile is horizontal "+
790 // "at the aft end of the transition. The angle at the fore end is defined "+
791 // "by the shape parameter.") {
793 // public boolean usesParameter() {
797 // public double maxParameter() {
798 // return 3.0; // Range 0...3
801 // public double defaultParameter() {
804 // public double getRadius(double x, double radius, double length, double param) {
806 // assert x <= length;
807 // assert radius >= 0;
808 // assert param >= 0;
809 // assert param <= 3;
810 // // p(x) = (k-2)x^3 + (3-2k)x^2 + k*x
812 // return radius*((((param-2)*x + (3-2*param))*x + param)*x);
817 // Privete fields of the shapes
818 private final String name;
819 private final String transitionDesc;
820 private final String noseconeDesc;
821 private final boolean canClip;
823 // Non-clippable constructor
824 Shape(String name, String noseconeDesc, String transitionDesc) {
825 this(name, noseconeDesc, transitionDesc, false);
828 // Clippable constructor
829 Shape(String name, String noseconeDesc, String transitionDesc, boolean canClip) {
831 this.canClip = canClip;
832 this.noseconeDesc = noseconeDesc;
833 this.transitionDesc = transitionDesc;
838 * Return the name of the transition shape name.
840 public String getName() {
845 * Get a description of the Transition shape.
847 public String getTransitionDescription() {
848 return transitionDesc;
852 * Get a description of the NoseCone shape.
854 public String getNoseConeDescription() {
859 * Check whether the shape differs in clipped mode. The clipping should be
860 * enabled by default if possible.
862 public boolean isClippable() {
867 * Return whether the shape uses the shape parameter. (Default false.)
869 public boolean usesParameter() {
874 * Return the minimum value of the shape parameter. (Default 0.)
876 public double minParameter() {
881 * Return the maximum value of the shape parameter. (Default 1.)
883 public double maxParameter() {
888 * Return the default value of the shape parameter. (Default 0.)
890 public double defaultParameter() {
895 * Calculate the basic radius of a transition with the given radius, length and
896 * shape parameter at the point x from the tip of the component. It is assumed
897 * that the fore radius if zero and the aft radius is <code>radius >= 0</code>.
898 * Boattails are achieved by reversing the component.
900 * @param x Position from the tip of the component.
901 * @param radius Aft end radius >= 0.
902 * @param length Length of the transition >= 0.
903 * @param param Valid shape parameter.
904 * @return The basic radius at the given position.
906 public abstract double getRadius(double x, double radius, double length, double param);
910 * Returns the name of the shape (same as getName()).
913 public String toString() {