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;
59 //////// Fore radius ////////
63 public double getForeRadius() {
64 if (isForeRadiusAutomatic()) {
65 // Get the automatic radius from the front
67 SymmetricComponent c = this.getPreviousSymmetricComponent();
69 r = c.getFrontAutoRadius();
78 public void setForeRadius(double radius) {
79 if ((this.radius1 == radius) && (autoRadius1 == false))
82 this.autoRadius1 = false;
83 this.radius1 = Math.max(radius, 0);
85 if (this.thickness > this.radius1 && this.thickness > this.radius2)
86 this.thickness = Math.max(this.radius1, this.radius2);
87 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
91 public boolean isForeRadiusAutomatic() {
95 public void setForeRadiusAutomatic(boolean auto) {
96 if (autoRadius1 == auto)
100 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
104 //////// Aft radius /////////
107 public double getAftRadius() {
108 if (isAftRadiusAutomatic()) {
109 // Return the auto radius from the rear
111 SymmetricComponent c = this.getNextSymmetricComponent();
113 r = c.getRearAutoRadius();
124 public void setAftRadius(double radius) {
125 if ((this.radius2 == radius) && (autoRadius2 == false))
128 this.autoRadius2 = false;
129 this.radius2 = Math.max(radius, 0);
131 if (this.thickness > this.radius1 && this.thickness > this.radius2)
132 this.thickness = Math.max(this.radius1, this.radius2);
133 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
137 public boolean isAftRadiusAutomatic() {
141 public void setAftRadiusAutomatic(boolean auto) {
142 if (autoRadius2 == auto)
146 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
151 //// Radius automatics
154 protected double getFrontAutoRadius() {
155 if (isAftRadiusAutomatic())
157 return getAftRadius();
162 protected double getRearAutoRadius() {
163 if (isForeRadiusAutomatic())
165 return getForeRadius();
171 //////// Type & shape /////////
173 public Shape getType() {
177 public void setType(Shape type) {
179 throw new IllegalArgumentException("setType called with null argument");
181 if (this.type == type)
184 this.clipped = type.isClippable();
185 this.shapeParameter = type.defaultParameter();
186 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
189 public double getShapeParameter() {
190 return shapeParameter;
193 public void setShapeParameter(double n) {
194 if (shapeParameter == n)
196 this.shapeParameter = MathUtil.clamp(n, type.minParameter(), type.maxParameter());
197 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
200 public boolean isClipped() {
201 if (!type.isClippable())
206 public void setClipped(boolean c) {
210 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
213 public boolean isClippedEnabled() {
214 return type.isClippable();
217 public double getShapeParameterMin() {
218 return type.minParameter();
221 public double getShapeParameterMax() {
222 return type.maxParameter();
226 //////// Shoulders ////////
228 public double getForeShoulderRadius() {
229 return foreShoulderRadius;
232 public void setForeShoulderRadius(double foreShoulderRadius) {
233 if (MathUtil.equals(this.foreShoulderRadius, foreShoulderRadius))
235 this.foreShoulderRadius = foreShoulderRadius;
236 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
239 public double getForeShoulderThickness() {
240 return foreShoulderThickness;
243 public void setForeShoulderThickness(double foreShoulderThickness) {
244 if (MathUtil.equals(this.foreShoulderThickness, foreShoulderThickness))
246 this.foreShoulderThickness = foreShoulderThickness;
247 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
250 public double getForeShoulderLength() {
251 return foreShoulderLength;
254 public void setForeShoulderLength(double foreShoulderLength) {
255 if (MathUtil.equals(this.foreShoulderLength, foreShoulderLength))
257 this.foreShoulderLength = foreShoulderLength;
258 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
261 public boolean isForeShoulderCapped() {
262 return foreShoulderCapped;
265 public void setForeShoulderCapped(boolean capped) {
266 if (this.foreShoulderCapped == capped)
268 this.foreShoulderCapped = capped;
269 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
275 public double getAftShoulderRadius() {
276 return aftShoulderRadius;
279 public void setAftShoulderRadius(double aftShoulderRadius) {
280 if (MathUtil.equals(this.aftShoulderRadius, aftShoulderRadius))
282 this.aftShoulderRadius = aftShoulderRadius;
283 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
286 public double getAftShoulderThickness() {
287 return aftShoulderThickness;
290 public void setAftShoulderThickness(double aftShoulderThickness) {
291 if (MathUtil.equals(this.aftShoulderThickness, aftShoulderThickness))
293 this.aftShoulderThickness = aftShoulderThickness;
294 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
297 public double getAftShoulderLength() {
298 return aftShoulderLength;
301 public void setAftShoulderLength(double aftShoulderLength) {
302 if (MathUtil.equals(this.aftShoulderLength, aftShoulderLength))
304 this.aftShoulderLength = aftShoulderLength;
305 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
308 public boolean isAftShoulderCapped() {
309 return aftShoulderCapped;
312 public void setAftShoulderCapped(boolean capped) {
313 if (this.aftShoulderCapped == capped)
315 this.aftShoulderCapped = capped;
316 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
322 /////////// Shape implementations ////////////
327 * Return the radius at point x of the transition.
330 public double getRadius(double x) {
331 if (x < 0 || x > length)
334 double r1 = getForeRadius();
335 double r2 = getAftRadius();
348 // Check clip calculation
350 calculateClip(r1, r2);
351 return type.getRadius(clipLength + x, r2, clipLength + length, shapeParameter);
354 return r1 + type.getRadius(x, r2 - r1, length, shapeParameter);
359 * Numerically solve clipLength from the equation
360 * r1 == type.getRadius(clipLength,r2,clipLength+length)
361 * using a binary search. It assumes getOuterRadius() to be monotonically increasing.
363 private void calculateClip(double r1, double r2) {
364 double min = 0, max = length;
383 // getR(min,min+length,r2) - r1 < 0
384 // getR(max,max+length,r2) - r1 > 0
387 while (type.getRadius(max, r2, max + length, shapeParameter) - r1 < 0) {
396 clipLength = (min + max) / 2;
397 if ((max - min) < CLIP_PRECISION)
399 double val = type.getRadius(clipLength, r2, clipLength + length, shapeParameter);
410 public double getInnerRadius(double x) {
411 return Math.max(getRadius(x) - thickness, 0);
417 public Collection<Coordinate> getComponentBounds() {
418 Collection<Coordinate> bounds = super.getComponentBounds();
419 if (foreShoulderLength > 0.001)
420 addBound(bounds, -foreShoulderLength, foreShoulderRadius);
421 if (aftShoulderLength > 0.001)
422 addBound(bounds, getLength() + aftShoulderLength, aftShoulderRadius);
427 public double getComponentMass() {
428 double mass = super.getComponentMass();
429 if (getForeShoulderLength() > 0.001) {
430 final double or = getForeShoulderRadius();
431 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
432 mass += ringMass(or, ir, getForeShoulderLength(), getMaterial().getDensity());
434 if (isForeShoulderCapped()) {
435 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
436 mass += ringMass(ir, 0, getForeShoulderThickness(), getMaterial().getDensity());
439 if (getAftShoulderLength() > 0.001) {
440 final double or = getAftShoulderRadius();
441 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
442 mass += ringMass(or, ir, getAftShoulderLength(), getMaterial().getDensity());
444 if (isAftShoulderCapped()) {
445 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
446 mass += ringMass(ir, 0, getAftShoulderThickness(), getMaterial().getDensity());
453 public Coordinate getComponentCG() {
454 Coordinate cg = super.getComponentCG();
455 if (getForeShoulderLength() > 0.001) {
456 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
457 cg = cg.average(ringCG(getForeShoulderRadius(), ir, -getForeShoulderLength(), 0,
458 getMaterial().getDensity()));
460 if (isForeShoulderCapped()) {
461 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
462 cg = cg.average(ringCG(ir, 0, -getForeShoulderLength(),
463 getForeShoulderThickness() - getForeShoulderLength(),
464 getMaterial().getDensity()));
467 if (getAftShoulderLength() > 0.001) {
468 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
469 cg = cg.average(ringCG(getAftShoulderRadius(), ir, getLength(),
470 getLength() + getAftShoulderLength(), getMaterial().getDensity()));
472 if (isAftShoulderCapped()) {
473 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
474 cg = cg.average(ringCG(ir, 0,
475 getLength() + getAftShoulderLength() - getAftShoulderThickness(),
476 getLength() + getAftShoulderLength(), getMaterial().getDensity()));
483 * The moments of inertia are not explicitly corrected for the shoulders.
484 * However, since the mass is corrected, the inertia is automatically corrected
485 * to very nearly the correct value.
491 * Returns the name of the component ("Transition").
494 public String getComponentName() {
496 return trans.get("Transition.Transition");
500 protected void componentChanged(ComponentChangeEvent e) {
501 super.componentChanged(e);
506 * Check whether the given type can be added to this component. Transitions allow any
507 * InternalComponents to be added.
509 * @param ctype The RocketComponent class type to add.
510 * @return Whether such a component can be added.
513 public boolean isCompatible(Class<? extends RocketComponent> ctype) {
514 if (InternalComponent.class.isAssignableFrom(ctype))
520 public Type getPresetType() {
521 return ComponentPreset.Type.TRANSITION;
526 protected void loadFromPreset(ComponentPreset preset) {
528 if ( preset.has(ComponentPreset.SHAPE) ) {
529 Shape s = preset.get(ComponentPreset.SHAPE);
532 if ( preset.has(ComponentPreset.OUTER_DIAMETER) ) {
533 double outerDiameter = preset.get(ComponentPreset.OUTER_DIAMETER);
534 this.setAftRadiusAutomatic(false);
535 this.setAftRadius(outerDiameter/2.0);
537 if ( preset.has(ComponentPreset.SHOULDER_LENGTH) ) {
538 double d = preset.get(ComponentPreset.SHOULDER_LENGTH);
539 this.setAftShoulderLength(d);
541 if ( preset.has(ComponentPreset.SHOULDER_DIAMETER) ) {
542 double d = preset.get(ComponentPreset.SHOULDER_DIAMETER);
543 this.setAftShoulderRadius(d/2.0);
545 if ( preset.has(ComponentPreset.FORE_OUTER_DIAMETER) ) {
546 double outerDiameter = preset.get(ComponentPreset.FORE_OUTER_DIAMETER);
547 this.setForeRadiusAutomatic(false);
548 this.setForeRadius(outerDiameter/2.0);
550 if ( preset.has(ComponentPreset.FORE_SHOULDER_LENGTH) ) {
551 double d = preset.get(ComponentPreset.FORE_SHOULDER_LENGTH);
552 this.setForeShoulderLength(d);
554 if ( preset.has(ComponentPreset.FORE_SHOULDER_DIAMETER) ) {
555 double d = preset.get(ComponentPreset.FORE_SHOULDER_DIAMETER);
556 this.setForeShoulderRadius(d/2.0);
559 super.loadFromPreset(preset);
561 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
566 * An enumeration listing the possible shapes of transitions.
568 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
570 public static enum Shape {
576 CONICAL(trans.get("Shape.Conical"),
577 //// A conical nose cone has a profile of a triangle.
578 trans.get("Shape.Conical.desc1"),
579 //// A conical transition has straight sides.
580 trans.get("Shape.Conical.desc2")) {
582 public double getRadius(double x, double radius, double length, double param) {
586 return radius * x / length;
591 * Ogive shape. The shape parameter is the portion of an extended tangent ogive
592 * that will be used. That is, for param==1 a tangent ogive will be produced, and
593 * for smaller values the shape straightens out into a cone at param==0.
596 OGIVE(trans.get("Shape.Ogive"),
597 //// 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>.
598 trans.get("Shape.Ogive.desc1"),
599 //// 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>.
600 trans.get("Shape.Ogive.desc2")) {
602 public boolean usesParameter() {
603 return true; // Range 0...1 is default
607 public double defaultParameter() {
608 return 1.0; // Tangent ogive by default
612 public double getRadius(double x, double radius, double length, double param) {
619 // Impossible to calculate ogive for length < radius, scale instead
620 // TODO: LOW: secant ogive could be calculated lower
621 if (length < radius) {
622 x = x * radius / length;
627 return CONICAL.getRadius(x, radius, length, param);
629 // Radius of circle is:
630 double R = MathUtil.safeSqrt((pow2(length) + pow2(radius)) *
631 (pow2((2 - param) * length) + pow2(param * radius)) / (4 * pow2(param * radius)));
632 double L = length / param;
633 // double R = (radius + length*length/(radius*param*param))/2;
634 double y0 = MathUtil.safeSqrt(R * R - L * L);
635 return MathUtil.safeSqrt(R * R - (L - x) * (L - x)) - y0;
643 ELLIPSOID(trans.get("Shape.Ellipsoid"),
644 //// An ellipsoidal nose cone has a profile of a half-ellipse with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>.
645 trans.get("Shape.Ellipsoid.desc1"),
646 //// 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.
647 trans.get("Shape.Ellipsoid.desc2"), true) {
649 public double getRadius(double x, double radius, double length, double param) {
653 x = x * radius / length;
654 return MathUtil.safeSqrt(2 * radius * x - x * x); // radius/length * sphere
659 POWER(trans.get("Shape.Powerseries"),
660 trans.get("Shape.Powerseries.desc1"),
661 trans.get("Shape.Powerseries.desc2"), true) {
663 public boolean usesParameter() { // Range 0...1
668 public double defaultParameter() {
673 public double getRadius(double x, double radius, double length, double param) {
679 if (param <= 0.00001) {
685 return radius * Math.pow(x / length, param);
690 //// Parabolic series
691 PARABOLIC(trans.get("Shape.Parabolicseries"),
692 ////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.
693 trans.get("Shape.Parabolicseries.desc1"),
694 ////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.
695 trans.get("Shape.Parabolicseries.desc2")) {
697 // In principle a parabolic transition is clippable, but the difference is
701 public boolean usesParameter() { // Range 0...1
706 public double defaultParameter() {
711 public double getRadius(double x, double radius, double length, double param) {
718 return radius * ((2 * x / length - param * pow2(x / length)) / (2 - param));
723 HAACK(trans.get("Shape.Haackseries"),
724 //// 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.
725 trans.get("Shape.Haackseries.desc1"),
726 //// 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.
727 trans.get("Shape.Haackseries.desc2"), true) {
730 public boolean usesParameter() {
735 public double maxParameter() {
736 return 1.0 / 3.0; // Range 0...1/3
740 public double getRadius(double x, double radius, double length, double param) {
747 double theta = Math.acos(1 - 2 * x / length);
748 if (MathUtil.equals(param, 0)) {
749 return radius * MathUtil.safeSqrt((theta - sin(2 * theta) / 2) / Math.PI);
751 return radius * MathUtil.safeSqrt((theta - sin(2 * theta) / 2 + param * pow3(sin(theta))) / Math.PI);
755 // POLYNOMIAL("Smooth polynomial",
756 // "A polynomial is fitted such that the nose cone profile is horizontal "+
757 // "at the aft end of the transition. The angle at the tip is defined by "+
758 // "the shape parameter.",
759 // "A polynomial is fitted such that the transition profile is horizontal "+
760 // "at the aft end of the transition. The angle at the fore end is defined "+
761 // "by the shape parameter.") {
763 // public boolean usesParameter() {
767 // public double maxParameter() {
768 // return 3.0; // Range 0...3
771 // public double defaultParameter() {
774 // public double getRadius(double x, double radius, double length, double param) {
776 // assert x <= length;
777 // assert radius >= 0;
778 // assert param >= 0;
779 // assert param <= 3;
780 // // p(x) = (k-2)x^3 + (3-2k)x^2 + k*x
782 // return radius*((((param-2)*x + (3-2*param))*x + param)*x);
787 // Privete fields of the shapes
788 private final String name;
789 private final String transitionDesc;
790 private final String noseconeDesc;
791 private final boolean canClip;
793 // Non-clippable constructor
794 Shape(String name, String noseconeDesc, String transitionDesc) {
795 this(name, noseconeDesc, transitionDesc, false);
798 // Clippable constructor
799 Shape(String name, String noseconeDesc, String transitionDesc, boolean canClip) {
801 this.canClip = canClip;
802 this.noseconeDesc = noseconeDesc;
803 this.transitionDesc = transitionDesc;
808 * Return the name of the transition shape name.
810 public String getName() {
815 * Get a description of the Transition shape.
817 public String getTransitionDescription() {
818 return transitionDesc;
822 * Get a description of the NoseCone shape.
824 public String getNoseConeDescription() {
829 * Check whether the shape differs in clipped mode. The clipping should be
830 * enabled by default if possible.
832 public boolean isClippable() {
837 * Return whether the shape uses the shape parameter. (Default false.)
839 public boolean usesParameter() {
844 * Return the minimum value of the shape parameter. (Default 0.)
846 public double minParameter() {
851 * Return the maximum value of the shape parameter. (Default 1.)
853 public double maxParameter() {
858 * Return the default value of the shape parameter. (Default 0.)
860 public double defaultParameter() {
865 * Calculate the basic radius of a transition with the given radius, length and
866 * shape parameter at the point x from the tip of the component. It is assumed
867 * that the fore radius if zero and the aft radius is <code>radius >= 0</code>.
868 * Boattails are achieved by reversing the component.
870 * @param x Position from the tip of the component.
871 * @param radius Aft end radius >= 0.
872 * @param length Length of the transition >= 0.
873 * @param param Valid shape parameter.
874 * @return The basic radius at the given position.
876 public abstract double getRadius(double x, double radius, double length, double param);
880 * Returns the name of the shape (same as getName()).
883 public String toString() {