1 package net.sf.openrocket.rocketcomponent;
3 import net.sf.openrocket.l10n.Translator;
4 import net.sf.openrocket.preset.ComponentPreset;
5 import net.sf.openrocket.preset.ComponentPreset.Type;
6 import net.sf.openrocket.startup.Application;
7 import net.sf.openrocket.util.Coordinate;
8 import net.sf.openrocket.util.MathUtil;
10 import java.util.Collection;
12 import static java.lang.Math.sin;
13 import static net.sf.openrocket.util.MathUtil.pow2;
14 import static net.sf.openrocket.util.MathUtil.pow3;
17 public class Transition extends SymmetricComponent {
18 private static final Translator trans = Application.getTranslator();
19 private static final double CLIP_PRECISION = 0.0001;
23 private double shapeParameter;
24 private boolean clipped; // Not to be read - use isClipped(), which may be overriden
26 private double radius1, radius2;
27 private boolean autoRadius1, autoRadius2; // Whether the start radius is automatic
30 private double foreShoulderRadius;
31 private double foreShoulderThickness;
32 private double foreShoulderLength;
33 private boolean foreShoulderCapped;
34 private double aftShoulderRadius;
35 private double aftShoulderThickness;
36 private double aftShoulderLength;
37 private boolean aftShoulderCapped;
40 // Used to cache the clip length
41 private double clipLength = -1;
46 this.radius1 = DEFAULT_RADIUS;
47 this.radius2 = DEFAULT_RADIUS;
48 this.length = DEFAULT_RADIUS * 3;
49 this.autoRadius1 = true;
50 this.autoRadius2 = true;
52 this.type = Shape.CONICAL;
53 this.shapeParameter = 0;
57 //////// Length ////////
59 public void setLength( double length ) {
60 if ( this.length == length ) {
63 // Need to clearPreset when length changes.
65 super.setLength( length );
69 //////// Fore radius ////////
73 public double getForeRadius() {
74 if (isForeRadiusAutomatic()) {
75 // Get the automatic radius from the front
77 SymmetricComponent c = this.getPreviousSymmetricComponent();
79 r = c.getFrontAutoRadius();
88 public void setForeRadius(double radius) {
89 if ((this.radius1 == radius) && (autoRadius1 == false))
92 this.autoRadius1 = false;
93 this.radius1 = Math.max(radius, 0);
95 if (this.thickness > this.radius1 && this.thickness > this.radius2)
96 this.thickness = Math.max(this.radius1, this.radius2);
99 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
103 public boolean isForeRadiusAutomatic() {
107 public void setForeRadiusAutomatic(boolean auto) {
108 if (autoRadius1 == auto)
114 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
118 //////// Aft radius /////////
121 public double getAftRadius() {
122 if (isAftRadiusAutomatic()) {
123 // Return the auto radius from the rear
125 SymmetricComponent c = this.getNextSymmetricComponent();
127 r = c.getRearAutoRadius();
138 public void setAftRadius(double radius) {
139 if ((this.radius2 == radius) && (autoRadius2 == false))
142 this.autoRadius2 = false;
143 this.radius2 = Math.max(radius, 0);
145 if (this.thickness > this.radius1 && this.thickness > this.radius2)
146 this.thickness = Math.max(this.radius1, this.radius2);
149 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
153 public boolean isAftRadiusAutomatic() {
157 public void setAftRadiusAutomatic(boolean auto) {
158 if (autoRadius2 == auto)
164 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
169 //// Radius automatics
172 protected double getFrontAutoRadius() {
173 if (isAftRadiusAutomatic())
175 return getAftRadius();
180 protected double getRearAutoRadius() {
181 if (isForeRadiusAutomatic())
183 return getForeRadius();
189 //////// Type & shape /////////
191 public Shape getType() {
195 public void setType(Shape type) {
197 throw new IllegalArgumentException("setType called with null argument");
199 if (this.type == type)
202 this.clipped = type.isClippable();
203 this.shapeParameter = type.defaultParameter();
204 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
207 public double getShapeParameter() {
208 return shapeParameter;
211 public void setShapeParameter(double n) {
212 if (shapeParameter == n)
214 this.shapeParameter = MathUtil.clamp(n, type.minParameter(), type.maxParameter());
215 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
218 public boolean isClipped() {
219 if (!type.isClippable())
224 public void setClipped(boolean c) {
228 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
231 public boolean isClippedEnabled() {
232 return type.isClippable();
235 public double getShapeParameterMin() {
236 return type.minParameter();
239 public double getShapeParameterMax() {
240 return type.maxParameter();
244 //////// Shoulders ////////
246 public double getForeShoulderRadius() {
247 return foreShoulderRadius;
250 public void setForeShoulderRadius(double foreShoulderRadius) {
251 if (MathUtil.equals(this.foreShoulderRadius, foreShoulderRadius))
253 this.foreShoulderRadius = foreShoulderRadius;
255 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
258 public double getForeShoulderThickness() {
259 return foreShoulderThickness;
262 public void setForeShoulderThickness(double foreShoulderThickness) {
263 if (MathUtil.equals(this.foreShoulderThickness, foreShoulderThickness))
265 this.foreShoulderThickness = foreShoulderThickness;
266 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
269 public double getForeShoulderLength() {
270 return foreShoulderLength;
273 public void setForeShoulderLength(double foreShoulderLength) {
274 if (MathUtil.equals(this.foreShoulderLength, foreShoulderLength))
276 this.foreShoulderLength = foreShoulderLength;
277 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
280 public boolean isForeShoulderCapped() {
281 return foreShoulderCapped;
284 public void setForeShoulderCapped(boolean capped) {
285 if (this.foreShoulderCapped == capped)
287 this.foreShoulderCapped = capped;
288 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
294 public double getAftShoulderRadius() {
295 return aftShoulderRadius;
298 public void setAftShoulderRadius(double aftShoulderRadius) {
299 if (MathUtil.equals(this.aftShoulderRadius, aftShoulderRadius))
301 this.aftShoulderRadius = aftShoulderRadius;
303 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
306 public double getAftShoulderThickness() {
307 return aftShoulderThickness;
310 public void setAftShoulderThickness(double aftShoulderThickness) {
311 if (MathUtil.equals(this.aftShoulderThickness, aftShoulderThickness))
313 this.aftShoulderThickness = aftShoulderThickness;
314 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
317 public double getAftShoulderLength() {
318 return aftShoulderLength;
321 public void setAftShoulderLength(double aftShoulderLength) {
322 if (MathUtil.equals(this.aftShoulderLength, aftShoulderLength))
324 this.aftShoulderLength = aftShoulderLength;
325 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
328 public boolean isAftShoulderCapped() {
329 return aftShoulderCapped;
332 public void setAftShoulderCapped(boolean capped) {
333 if (this.aftShoulderCapped == capped)
335 this.aftShoulderCapped = capped;
336 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
342 /////////// Shape implementations ////////////
347 * Return the radius at point x of the transition.
350 public double getRadius(double x) {
351 if (x < 0 || x > length)
354 double r1 = getForeRadius();
355 double r2 = getAftRadius();
368 // Check clip calculation
370 calculateClip(r1, r2);
371 return type.getRadius(clipLength + x, r2, clipLength + length, shapeParameter);
374 return r1 + type.getRadius(x, r2 - r1, length, shapeParameter);
379 * Numerically solve clipLength from the equation
380 * r1 == type.getRadius(clipLength,r2,clipLength+length)
381 * using a binary search. It assumes getOuterRadius() to be monotonically increasing.
383 private void calculateClip(double r1, double r2) {
384 double min = 0, max = length;
403 // getR(min,min+length,r2) - r1 < 0
404 // getR(max,max+length,r2) - r1 > 0
407 while (type.getRadius(max, r2, max + length, shapeParameter) - r1 < 0) {
416 clipLength = (min + max) / 2;
417 if ((max - min) < CLIP_PRECISION)
419 double val = type.getRadius(clipLength, r2, clipLength + length, shapeParameter);
430 public double getInnerRadius(double x) {
431 return Math.max(getRadius(x) - thickness, 0);
437 public Collection<Coordinate> getComponentBounds() {
438 Collection<Coordinate> bounds = super.getComponentBounds();
439 if (foreShoulderLength > 0.001)
440 addBound(bounds, -foreShoulderLength, foreShoulderRadius);
441 if (aftShoulderLength > 0.001)
442 addBound(bounds, getLength() + aftShoulderLength, aftShoulderRadius);
447 public double getComponentVolume() {
448 double volume = super.getComponentVolume();
449 if (getForeShoulderLength() > 0.001) {
450 final double or = getForeShoulderRadius();
451 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
452 volume += ringVolume( or, ir, getForeShoulderLength() );
454 if (isForeShoulderCapped()) {
455 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
456 volume += ringVolume(ir, 0, getForeShoulderThickness() );
459 if (getAftShoulderLength() > 0.001) {
460 final double or = getAftShoulderRadius();
461 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
462 volume += ringVolume(or, ir, getAftShoulderLength() );
464 if (isAftShoulderCapped()) {
465 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
466 volume += ringVolume(ir, 0, getAftShoulderThickness() );
473 public Coordinate getComponentCG() {
474 Coordinate cg = super.getComponentCG();
475 if (getForeShoulderLength() > 0.001) {
476 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
477 cg = cg.average(ringCG(getForeShoulderRadius(), ir, -getForeShoulderLength(), 0,
478 getMaterial().getDensity()));
480 if (isForeShoulderCapped()) {
481 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
482 cg = cg.average(ringCG(ir, 0, -getForeShoulderLength(),
483 getForeShoulderThickness() - getForeShoulderLength(),
484 getMaterial().getDensity()));
487 if (getAftShoulderLength() > 0.001) {
488 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
489 cg = cg.average(ringCG(getAftShoulderRadius(), ir, getLength(),
490 getLength() + getAftShoulderLength(), getMaterial().getDensity()));
492 if (isAftShoulderCapped()) {
493 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
494 cg = cg.average(ringCG(ir, 0,
495 getLength() + getAftShoulderLength() - getAftShoulderThickness(),
496 getLength() + getAftShoulderLength(), getMaterial().getDensity()));
503 * The moments of inertia are not explicitly corrected for the shoulders.
504 * However, since the mass is corrected, the inertia is automatically corrected
505 * to very nearly the correct value.
511 * Returns the name of the component ("Transition").
514 public String getComponentName() {
516 return trans.get("Transition.Transition");
520 protected void componentChanged(ComponentChangeEvent e) {
521 super.componentChanged(e);
526 * Check whether the given type can be added to this component. Transitions allow any
527 * InternalComponents to be added.
529 * @param ctype The RocketComponent class type to add.
530 * @return Whether such a component can be added.
533 public boolean isCompatible(Class<? extends RocketComponent> ctype) {
534 if (InternalComponent.class.isAssignableFrom(ctype))
540 public Type getPresetType() {
541 return ComponentPreset.Type.TRANSITION;
546 protected void loadFromPreset(ComponentPreset preset) {
548 boolean presetFilled = false;
549 if ( preset.has(ComponentPreset.FILLED ) ) {
550 presetFilled = preset.get( ComponentPreset.FILLED);
553 if ( preset.has(ComponentPreset.SHAPE) ) {
554 Shape s = preset.get(ComponentPreset.SHAPE);
557 if ( preset.has(ComponentPreset.AFT_OUTER_DIAMETER) ) {
558 double outerDiameter = preset.get(ComponentPreset.AFT_OUTER_DIAMETER);
559 this.setAftRadiusAutomatic(false);
560 this.setAftRadius(outerDiameter/2.0);
562 if ( preset.has(ComponentPreset.AFT_SHOULDER_LENGTH) ) {
563 double d = preset.get(ComponentPreset.AFT_SHOULDER_LENGTH);
564 this.setAftShoulderLength(d);
566 if ( preset.has(ComponentPreset.AFT_SHOULDER_DIAMETER) ) {
567 double d = preset.get(ComponentPreset.AFT_SHOULDER_DIAMETER);
568 this.setAftShoulderRadius(d/2.0);
569 if ( presetFilled ) {
570 this.setAftShoulderThickness(d/2.0);
573 if ( preset.has(ComponentPreset.FORE_OUTER_DIAMETER) ) {
574 double outerDiameter = preset.get(ComponentPreset.FORE_OUTER_DIAMETER);
575 this.setForeRadiusAutomatic(false);
576 this.setForeRadius(outerDiameter/2.0);
578 if ( preset.has(ComponentPreset.FORE_SHOULDER_LENGTH) ) {
579 double d = preset.get(ComponentPreset.FORE_SHOULDER_LENGTH);
580 this.setForeShoulderLength(d);
582 if ( preset.has(ComponentPreset.FORE_SHOULDER_DIAMETER) ) {
583 double d = preset.get(ComponentPreset.FORE_SHOULDER_DIAMETER);
584 this.setForeShoulderRadius(d/2.0);
585 if ( presetFilled ) {
586 this.setForeShoulderThickness(d/2.0);
590 super.loadFromPreset(preset);
592 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
597 * An enumeration listing the possible shapes of transitions.
599 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
601 public static enum Shape {
607 CONICAL(trans.get("Shape.Conical"),
608 //// A conical nose cone has a profile of a triangle.
609 trans.get("Shape.Conical.desc1"),
610 //// A conical transition has straight sides.
611 trans.get("Shape.Conical.desc2")) {
613 public double getRadius(double x, double radius, double length, double param) {
617 return radius * x / length;
622 * Ogive shape. The shape parameter is the portion of an extended tangent ogive
623 * that will be used. That is, for param==1 a tangent ogive will be produced, and
624 * for smaller values the shape straightens out into a cone at param==0.
627 OGIVE(trans.get("Shape.Ogive"),
628 //// 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>.
629 trans.get("Shape.Ogive.desc1"),
630 //// 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>.
631 trans.get("Shape.Ogive.desc2")) {
633 public boolean usesParameter() {
634 return true; // Range 0...1 is default
638 public double defaultParameter() {
639 return 1.0; // Tangent ogive by default
643 public double getRadius(double x, double radius, double length, double param) {
650 // Impossible to calculate ogive for length < radius, scale instead
651 // TODO: LOW: secant ogive could be calculated lower
652 if (length < radius) {
653 x = x * radius / length;
658 return CONICAL.getRadius(x, radius, length, param);
660 // Radius of circle is:
661 double R = MathUtil.safeSqrt((pow2(length) + pow2(radius)) *
662 (pow2((2 - param) * length) + pow2(param * radius)) / (4 * pow2(param * radius)));
663 double L = length / param;
664 // double R = (radius + length*length/(radius*param*param))/2;
665 double y0 = MathUtil.safeSqrt(R * R - L * L);
666 return MathUtil.safeSqrt(R * R - (L - x) * (L - x)) - y0;
674 ELLIPSOID(trans.get("Shape.Ellipsoid"),
675 //// An ellipsoidal nose cone has a profile of a half-ellipse with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>.
676 trans.get("Shape.Ellipsoid.desc1"),
677 //// 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.
678 trans.get("Shape.Ellipsoid.desc2"), true) {
680 public double getRadius(double x, double radius, double length, double param) {
684 x = x * radius / length;
685 return MathUtil.safeSqrt(2 * radius * x - x * x); // radius/length * sphere
690 POWER(trans.get("Shape.Powerseries"),
691 trans.get("Shape.Powerseries.desc1"),
692 trans.get("Shape.Powerseries.desc2"), true) {
694 public boolean usesParameter() { // Range 0...1
699 public double defaultParameter() {
704 public double getRadius(double x, double radius, double length, double param) {
710 if (param <= 0.00001) {
716 return radius * Math.pow(x / length, param);
721 //// Parabolic series
722 PARABOLIC(trans.get("Shape.Parabolicseries"),
723 ////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.
724 trans.get("Shape.Parabolicseries.desc1"),
725 ////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.
726 trans.get("Shape.Parabolicseries.desc2")) {
728 // In principle a parabolic transition is clippable, but the difference is
732 public boolean usesParameter() { // Range 0...1
737 public double defaultParameter() {
742 public double getRadius(double x, double radius, double length, double param) {
749 return radius * ((2 * x / length - param * pow2(x / length)) / (2 - param));
754 HAACK(trans.get("Shape.Haackseries"),
755 //// 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.
756 trans.get("Shape.Haackseries.desc1"),
757 //// 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.
758 trans.get("Shape.Haackseries.desc2"), true) {
761 public boolean usesParameter() {
766 public double maxParameter() {
767 return 1.0 / 3.0; // Range 0...1/3
771 public double getRadius(double x, double radius, double length, double param) {
778 double theta = Math.acos(1 - 2 * x / length);
779 if (MathUtil.equals(param, 0)) {
780 return radius * MathUtil.safeSqrt((theta - sin(2 * theta) / 2) / Math.PI);
782 return radius * MathUtil.safeSqrt((theta - sin(2 * theta) / 2 + param * pow3(sin(theta))) / Math.PI);
786 // POLYNOMIAL("Smooth polynomial",
787 // "A polynomial is fitted such that the nose cone profile is horizontal "+
788 // "at the aft end of the transition. The angle at the tip is defined by "+
789 // "the shape parameter.",
790 // "A polynomial is fitted such that the transition profile is horizontal "+
791 // "at the aft end of the transition. The angle at the fore end is defined "+
792 // "by the shape parameter.") {
794 // public boolean usesParameter() {
798 // public double maxParameter() {
799 // return 3.0; // Range 0...3
802 // public double defaultParameter() {
805 // public double getRadius(double x, double radius, double length, double param) {
807 // assert x <= length;
808 // assert radius >= 0;
809 // assert param >= 0;
810 // assert param <= 3;
811 // // p(x) = (k-2)x^3 + (3-2k)x^2 + k*x
813 // return radius*((((param-2)*x + (3-2*param))*x + param)*x);
818 // Privete fields of the shapes
819 private final String name;
820 private final String transitionDesc;
821 private final String noseconeDesc;
822 private final boolean canClip;
824 // Non-clippable constructor
825 Shape(String name, String noseconeDesc, String transitionDesc) {
826 this(name, noseconeDesc, transitionDesc, false);
829 // Clippable constructor
830 Shape(String name, String noseconeDesc, String transitionDesc, boolean canClip) {
832 this.canClip = canClip;
833 this.noseconeDesc = noseconeDesc;
834 this.transitionDesc = transitionDesc;
839 * Return the name of the transition shape name.
841 public String getName() {
846 * Get a description of the Transition shape.
848 public String getTransitionDescription() {
849 return transitionDesc;
853 * Get a description of the NoseCone shape.
855 public String getNoseConeDescription() {
860 * Check whether the shape differs in clipped mode. The clipping should be
861 * enabled by default if possible.
863 public boolean isClippable() {
868 * Return whether the shape uses the shape parameter. (Default false.)
870 public boolean usesParameter() {
875 * Return the minimum value of the shape parameter. (Default 0.)
877 public double minParameter() {
882 * Return the maximum value of the shape parameter. (Default 1.)
884 public double maxParameter() {
889 * Return the default value of the shape parameter. (Default 0.)
891 public double defaultParameter() {
896 * Calculate the basic radius of a transition with the given radius, length and
897 * shape parameter at the point x from the tip of the component. It is assumed
898 * that the fore radius if zero and the aft radius is <code>radius >= 0</code>.
899 * Boattails are achieved by reversing the component.
901 * @param x Position from the tip of the component.
902 * @param radius Aft end radius >= 0.
903 * @param length Length of the transition >= 0.
904 * @param param Valid shape parameter.
905 * @return The basic radius at the given position.
907 public abstract double getRadius(double x, double radius, double length, double param);
911 * Returns the name of the shape (same as getName()).
914 public String toString() {
919 * Lookup the Shape given the localized name. This differs from the standard valueOf as that looks up
920 * based on the canonical name, not the localized name which is an instance var.
922 * @param localizedName
925 public static Shape toShape(String localizedName) {
926 Shape[] values = Shape.values();
927 for (Shape value : values) {
928 if (value.getName().equals(localizedName)) {