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.startup.Application;
10 import net.sf.openrocket.util.Coordinate;
11 import net.sf.openrocket.util.MathUtil;
14 public class Transition extends SymmetricComponent {
15 private static final Translator trans = Application.getTranslator();
16 private static final double CLIP_PRECISION = 0.0001;
20 private double shapeParameter;
21 private boolean clipped; // Not to be read - use isClipped(), which may be overriden
23 private double radius1, radius2;
24 private boolean autoRadius1, autoRadius2; // Whether the start radius is automatic
27 private double foreShoulderRadius;
28 private double foreShoulderThickness;
29 private double foreShoulderLength;
30 private boolean foreShoulderCapped;
31 private double aftShoulderRadius;
32 private double aftShoulderThickness;
33 private double aftShoulderLength;
34 private boolean aftShoulderCapped;
37 // Used to cache the clip length
38 private double clipLength = -1;
43 this.radius1 = DEFAULT_RADIUS;
44 this.radius2 = DEFAULT_RADIUS;
45 this.length = DEFAULT_RADIUS * 3;
46 this.autoRadius1 = true;
47 this.autoRadius2 = true;
49 this.type = Shape.CONICAL;
50 this.shapeParameter = 0;
57 //////// Fore radius ////////
61 public double getForeRadius() {
62 if (isForeRadiusAutomatic()) {
63 // Get the automatic radius from the front
65 SymmetricComponent c = this.getPreviousSymmetricComponent();
67 r = c.getFrontAutoRadius();
76 public void setForeRadius(double radius) {
77 if ((this.radius1 == radius) && (autoRadius1 == false))
80 this.autoRadius1 = false;
81 this.radius1 = Math.max(radius, 0);
83 if (this.thickness > this.radius1 && this.thickness > this.radius2)
84 this.thickness = Math.max(this.radius1, this.radius2);
85 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
89 public boolean isForeRadiusAutomatic() {
93 public void setForeRadiusAutomatic(boolean auto) {
94 if (autoRadius1 == auto)
98 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
102 //////// Aft radius /////////
105 public double getAftRadius() {
106 if (isAftRadiusAutomatic()) {
107 // Return the auto radius from the rear
109 SymmetricComponent c = this.getNextSymmetricComponent();
111 r = c.getRearAutoRadius();
122 public void setAftRadius(double radius) {
123 if ((this.radius2 == radius) && (autoRadius2 == false))
126 this.autoRadius2 = false;
127 this.radius2 = Math.max(radius, 0);
129 if (this.thickness > this.radius1 && this.thickness > this.radius2)
130 this.thickness = Math.max(this.radius1, this.radius2);
131 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
135 public boolean isAftRadiusAutomatic() {
139 public void setAftRadiusAutomatic(boolean auto) {
140 if (autoRadius2 == auto)
144 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
149 //// Radius automatics
152 protected double getFrontAutoRadius() {
153 if (isAftRadiusAutomatic())
155 return getAftRadius();
160 protected double getRearAutoRadius() {
161 if (isForeRadiusAutomatic())
163 return getForeRadius();
169 //////// Type & shape /////////
171 public Shape getType() {
175 public void setType(Shape type) {
177 throw new IllegalArgumentException("setType called with null argument");
179 if (this.type == type)
182 this.clipped = type.isClippable();
183 this.shapeParameter = type.defaultParameter();
184 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
187 public double getShapeParameter() {
188 return shapeParameter;
191 public void setShapeParameter(double n) {
192 if (shapeParameter == n)
194 this.shapeParameter = MathUtil.clamp(n, type.minParameter(), type.maxParameter());
195 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
198 public boolean isClipped() {
199 if (!type.isClippable())
204 public void setClipped(boolean c) {
208 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
211 public boolean isClippedEnabled() {
212 return type.isClippable();
215 public double getShapeParameterMin() {
216 return type.minParameter();
219 public double getShapeParameterMax() {
220 return type.maxParameter();
224 //////// Shoulders ////////
226 public double getForeShoulderRadius() {
227 return foreShoulderRadius;
230 public void setForeShoulderRadius(double foreShoulderRadius) {
231 if (MathUtil.equals(this.foreShoulderRadius, foreShoulderRadius))
233 this.foreShoulderRadius = foreShoulderRadius;
234 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
237 public double getForeShoulderThickness() {
238 return foreShoulderThickness;
241 public void setForeShoulderThickness(double foreShoulderThickness) {
242 if (MathUtil.equals(this.foreShoulderThickness, foreShoulderThickness))
244 this.foreShoulderThickness = foreShoulderThickness;
245 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
248 public double getForeShoulderLength() {
249 return foreShoulderLength;
252 public void setForeShoulderLength(double foreShoulderLength) {
253 if (MathUtil.equals(this.foreShoulderLength, foreShoulderLength))
255 this.foreShoulderLength = foreShoulderLength;
256 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
259 public boolean isForeShoulderCapped() {
260 return foreShoulderCapped;
263 public void setForeShoulderCapped(boolean capped) {
264 if (this.foreShoulderCapped == capped)
266 this.foreShoulderCapped = capped;
267 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
273 public double getAftShoulderRadius() {
274 return aftShoulderRadius;
277 public void setAftShoulderRadius(double aftShoulderRadius) {
278 if (MathUtil.equals(this.aftShoulderRadius, aftShoulderRadius))
280 this.aftShoulderRadius = aftShoulderRadius;
281 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
284 public double getAftShoulderThickness() {
285 return aftShoulderThickness;
288 public void setAftShoulderThickness(double aftShoulderThickness) {
289 if (MathUtil.equals(this.aftShoulderThickness, aftShoulderThickness))
291 this.aftShoulderThickness = aftShoulderThickness;
292 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
295 public double getAftShoulderLength() {
296 return aftShoulderLength;
299 public void setAftShoulderLength(double aftShoulderLength) {
300 if (MathUtil.equals(this.aftShoulderLength, aftShoulderLength))
302 this.aftShoulderLength = aftShoulderLength;
303 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
306 public boolean isAftShoulderCapped() {
307 return aftShoulderCapped;
310 public void setAftShoulderCapped(boolean capped) {
311 if (this.aftShoulderCapped == capped)
313 this.aftShoulderCapped = capped;
314 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
320 /////////// Shape implementations ////////////
325 * Return the radius at point x of the transition.
328 public double getRadius(double x) {
329 if (x < 0 || x > length)
332 double r1 = getForeRadius();
333 double r2 = getAftRadius();
346 // Check clip calculation
348 calculateClip(r1, r2);
349 return type.getRadius(clipLength + x, r2, clipLength + length, shapeParameter);
352 return r1 + type.getRadius(x, r2 - r1, length, shapeParameter);
357 * Numerically solve clipLength from the equation
358 * r1 == type.getRadius(clipLength,r2,clipLength+length)
359 * using a binary search. It assumes getOuterRadius() to be monotonically increasing.
361 private void calculateClip(double r1, double r2) {
362 double min = 0, max = length;
381 // getR(min,min+length,r2) - r1 < 0
382 // getR(max,max+length,r2) - r1 > 0
385 while (type.getRadius(max, r2, max + length, shapeParameter) - r1 < 0) {
394 clipLength = (min + max) / 2;
395 if ((max - min) < CLIP_PRECISION)
397 double val = type.getRadius(clipLength, r2, clipLength + length, shapeParameter);
408 public double getInnerRadius(double x) {
409 return Math.max(getRadius(x) - thickness, 0);
415 public Collection<Coordinate> getComponentBounds() {
416 Collection<Coordinate> bounds = super.getComponentBounds();
417 if (foreShoulderLength > 0.001)
418 addBound(bounds, -foreShoulderLength, foreShoulderRadius);
419 if (aftShoulderLength > 0.001)
420 addBound(bounds, getLength() + aftShoulderLength, aftShoulderRadius);
425 public double getComponentMass() {
426 double mass = super.getComponentMass();
427 if (getForeShoulderLength() > 0.001) {
428 final double or = getForeShoulderRadius();
429 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
430 mass += ringMass(or, ir, getForeShoulderLength(), getMaterial().getDensity());
432 if (isForeShoulderCapped()) {
433 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
434 mass += ringMass(ir, 0, getForeShoulderThickness(), getMaterial().getDensity());
437 if (getAftShoulderLength() > 0.001) {
438 final double or = getAftShoulderRadius();
439 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
440 mass += ringMass(or, ir, getAftShoulderLength(), getMaterial().getDensity());
442 if (isAftShoulderCapped()) {
443 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
444 mass += ringMass(ir, 0, getAftShoulderThickness(), getMaterial().getDensity());
451 public Coordinate getComponentCG() {
452 Coordinate cg = super.getComponentCG();
453 if (getForeShoulderLength() > 0.001) {
454 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
455 cg = cg.average(ringCG(getForeShoulderRadius(), ir, -getForeShoulderLength(), 0,
456 getMaterial().getDensity()));
458 if (isForeShoulderCapped()) {
459 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
460 cg = cg.average(ringCG(ir, 0, -getForeShoulderLength(),
461 getForeShoulderThickness() - getForeShoulderLength(),
462 getMaterial().getDensity()));
465 if (getAftShoulderLength() > 0.001) {
466 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
467 cg = cg.average(ringCG(getAftShoulderRadius(), ir, getLength(),
468 getLength() + getAftShoulderLength(), getMaterial().getDensity()));
470 if (isAftShoulderCapped()) {
471 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
472 cg = cg.average(ringCG(ir, 0,
473 getLength() + getAftShoulderLength() - getAftShoulderThickness(),
474 getLength() + getAftShoulderLength(), getMaterial().getDensity()));
481 * The moments of inertia are not explicitly corrected for the shoulders.
482 * However, since the mass is corrected, the inertia is automatically corrected
483 * to very nearly the correct value.
489 * Returns the name of the component ("Transition").
492 public String getComponentName() {
494 return trans.get("Transition.Transition");
498 protected void componentChanged(ComponentChangeEvent e) {
499 super.componentChanged(e);
504 * Check whether the given type can be added to this component. Transitions allow any
505 * InternalComponents to be added.
507 * @param ctype The RocketComponent class type to add.
508 * @return Whether such a component can be added.
511 public boolean isCompatible(Class<? extends RocketComponent> ctype) {
512 if (InternalComponent.class.isAssignableFrom(ctype))
520 * An enumeration listing the possible shapes of transitions.
522 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
524 public static enum Shape {
530 CONICAL(trans.get("Shape.Conical"),
531 //// A conical nose cone has a profile of a triangle.
532 trans.get("Shape.Conical.desc1"),
533 //// A conical transition has straight sides.
534 trans.get("Shape.Conical.desc2")) {
536 public double getRadius(double x, double radius, double length, double param) {
540 return radius * x / length;
545 * Ogive shape. The shape parameter is the portion of an extended tangent ogive
546 * that will be used. That is, for param==1 a tangent ogive will be produced, and
547 * for smaller values the shape straightens out into a cone at param==0.
550 OGIVE(trans.get("Shape.Ogive"),
551 //// 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>.
552 trans.get("Shape.Ogive.desc1"),
553 //// 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>.
554 trans.get("Shape.Ogive.desc2")) {
556 public boolean usesParameter() {
557 return true; // Range 0...1 is default
561 public double defaultParameter() {
562 return 1.0; // Tangent ogive by default
566 public double getRadius(double x, double radius, double length, double param) {
573 // Impossible to calculate ogive for length < radius, scale instead
574 // TODO: LOW: secant ogive could be calculated lower
575 if (length < radius) {
576 x = x * radius / length;
581 return CONICAL.getRadius(x, radius, length, param);
583 // Radius of circle is:
584 double R = MathUtil.safeSqrt((pow2(length) + pow2(radius)) *
585 (pow2((2 - param) * length) + pow2(param * radius)) / (4 * pow2(param * radius)));
586 double L = length / param;
587 // double R = (radius + length*length/(radius*param*param))/2;
588 double y0 = MathUtil.safeSqrt(R * R - L * L);
589 return MathUtil.safeSqrt(R * R - (L - x) * (L - x)) - y0;
597 ELLIPSOID(trans.get("Shape.Ellipsoid"),
598 //// An ellipsoidal nose cone has a profile of a half-ellipse with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>.
599 trans.get("Shape.Ellipsoid.desc1"),
600 //// 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.
601 trans.get("Shape.Ellipsoid.desc2"), true) {
603 public double getRadius(double x, double radius, double length, double param) {
607 x = x * radius / length;
608 return MathUtil.safeSqrt(2 * radius * x - x * x); // radius/length * sphere
613 POWER(trans.get("Shape.Powerseries"),
614 trans.get("Shape.Powerseries.desc1"),
615 trans.get("Shape.Powerseries.desc2"), true) {
617 public boolean usesParameter() { // Range 0...1
622 public double defaultParameter() {
627 public double getRadius(double x, double radius, double length, double param) {
633 if (param <= 0.00001) {
639 return radius * Math.pow(x / length, param);
644 //// Parabolic series
645 PARABOLIC(trans.get("Shape.Parabolicseries"),
646 ////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.
647 trans.get("Shape.Parabolicseries.desc1"),
648 ////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.
649 trans.get("Shape.Parabolicseries.desc2")) {
651 // In principle a parabolic transition is clippable, but the difference is
655 public boolean usesParameter() { // Range 0...1
660 public double defaultParameter() {
665 public double getRadius(double x, double radius, double length, double param) {
672 return radius * ((2 * x / length - param * pow2(x / length)) / (2 - param));
677 HAACK(trans.get("Shape.Haackseries"),
678 //// 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.
679 trans.get("Shape.Haackseries.desc1"),
680 //// 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.
681 trans.get("Shape.Haackseries.desc2"), true) {
684 public boolean usesParameter() {
689 public double maxParameter() {
690 return 1.0 / 3.0; // Range 0...1/3
694 public double getRadius(double x, double radius, double length, double param) {
701 double theta = Math.acos(1 - 2 * x / length);
702 if (MathUtil.equals(param, 0)) {
703 return radius * MathUtil.safeSqrt((theta - sin(2 * theta) / 2) / Math.PI);
705 return radius * MathUtil.safeSqrt((theta - sin(2 * theta) / 2 + param * pow3(sin(theta))) / Math.PI);
709 // POLYNOMIAL("Smooth polynomial",
710 // "A polynomial is fitted such that the nose cone profile is horizontal "+
711 // "at the aft end of the transition. The angle at the tip is defined by "+
712 // "the shape parameter.",
713 // "A polynomial is fitted such that the transition profile is horizontal "+
714 // "at the aft end of the transition. The angle at the fore end is defined "+
715 // "by the shape parameter.") {
717 // public boolean usesParameter() {
721 // public double maxParameter() {
722 // return 3.0; // Range 0...3
725 // public double defaultParameter() {
728 // public double getRadius(double x, double radius, double length, double param) {
730 // assert x <= length;
731 // assert radius >= 0;
732 // assert param >= 0;
733 // assert param <= 3;
734 // // p(x) = (k-2)x^3 + (3-2k)x^2 + k*x
736 // return radius*((((param-2)*x + (3-2*param))*x + param)*x);
741 // Privete fields of the shapes
742 private final String name;
743 private final String transitionDesc;
744 private final String noseconeDesc;
745 private final boolean canClip;
747 // Non-clippable constructor
748 Shape(String name, String noseconeDesc, String transitionDesc) {
749 this(name, noseconeDesc, transitionDesc, false);
752 // Clippable constructor
753 Shape(String name, String noseconeDesc, String transitionDesc, boolean canClip) {
755 this.canClip = canClip;
756 this.noseconeDesc = noseconeDesc;
757 this.transitionDesc = transitionDesc;
762 * Return the name of the transition shape name.
764 public String getName() {
769 * Get a description of the Transition shape.
771 public String getTransitionDescription() {
772 return transitionDesc;
776 * Get a description of the NoseCone shape.
778 public String getNoseConeDescription() {
783 * Check whether the shape differs in clipped mode. The clipping should be
784 * enabled by default if possible.
786 public boolean isClippable() {
791 * Return whether the shape uses the shape parameter. (Default false.)
793 public boolean usesParameter() {
798 * Return the minimum value of the shape parameter. (Default 0.)
800 public double minParameter() {
805 * Return the maximum value of the shape parameter. (Default 1.)
807 public double maxParameter() {
812 * Return the default value of the shape parameter. (Default 0.)
814 public double defaultParameter() {
819 * Calculate the basic radius of a transition with the given radius, length and
820 * shape parameter at the point x from the tip of the component. It is assumed
821 * that the fore radius if zero and the aft radius is <code>radius >= 0</code>.
822 * Boattails are achieved by reversing the component.
824 * @param x Position from the tip of the component.
825 * @param radius Aft end radius >= 0.
826 * @param length Length of the transition >= 0.
827 * @param param Valid shape parameter.
828 * @return The basic radius at the given position.
830 public abstract double getRadius(double x, double radius, double length, double param);
834 * Returns the name of the shape (same as getName()).
837 public String toString() {