1 package net.sf.openrocket.rocketcomponent;
3 import net.sf.openrocket.l10n.Translator;
4 import net.sf.openrocket.startup.Application;
5 import net.sf.openrocket.util.Coordinate;
6 import net.sf.openrocket.util.MathUtil;
8 import java.util.Collection;
10 import static java.lang.Math.sin;
11 import static java.lang.Math.sqrt;
12 import static net.sf.openrocket.util.Chars.FRAC12;
13 import static net.sf.openrocket.util.Chars.FRAC34;
14 import static net.sf.openrocket.util.MathUtil.pow2;
15 import static net.sf.openrocket.util.MathUtil.pow3;
18 public class Transition extends SymmetricComponent {
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;
38 private static final Translator trans = Application.getTranslator();
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;
60 //////// Fore radius ////////
64 public double getForeRadius() {
65 if (isForeRadiusAutomatic()) {
66 // Get the automatic radius from the front
68 SymmetricComponent c = this.getPreviousSymmetricComponent();
70 r = c.getFrontAutoRadius();
79 public void setForeRadius(double radius) {
80 if ((this.radius1 == radius) && (autoRadius1 == false))
83 this.autoRadius1 = false;
84 this.radius1 = Math.max(radius, 0);
86 if (this.thickness > this.radius1 && this.thickness > this.radius2)
87 this.thickness = Math.max(this.radius1, this.radius2);
88 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
92 public boolean isForeRadiusAutomatic() {
96 public void setForeRadiusAutomatic(boolean auto) {
97 if (autoRadius1 == auto)
101 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
105 //////// Aft radius /////////
108 public double getAftRadius() {
109 if (isAftRadiusAutomatic()) {
110 // Return the auto radius from the rear
112 SymmetricComponent c = this.getNextSymmetricComponent();
114 r = c.getRearAutoRadius();
125 public void setAftRadius(double radius) {
126 if ((this.radius2 == radius) && (autoRadius2 == false))
129 this.autoRadius2 = false;
130 this.radius2 = Math.max(radius, 0);
132 if (this.thickness > this.radius1 && this.thickness > this.radius2)
133 this.thickness = Math.max(this.radius1, this.radius2);
134 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
138 public boolean isAftRadiusAutomatic() {
142 public void setAftRadiusAutomatic(boolean auto) {
143 if (autoRadius2 == auto)
147 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
152 //// Radius automatics
155 protected double getFrontAutoRadius() {
156 if (isAftRadiusAutomatic())
158 return getAftRadius();
163 protected double getRearAutoRadius() {
164 if (isForeRadiusAutomatic())
166 return getForeRadius();
172 //////// Type & shape /////////
174 public Shape getType() {
178 public void setType(Shape type) {
180 throw new IllegalArgumentException("setType called with null argument");
182 if (this.type == type)
185 this.clipped = type.isClippable();
186 this.shapeParameter = type.defaultParameter();
187 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
190 public double getShapeParameter() {
191 return shapeParameter;
194 public void setShapeParameter(double n) {
195 if (shapeParameter == n)
197 this.shapeParameter = MathUtil.clamp(n, type.minParameter(), type.maxParameter());
198 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
201 public boolean isClipped() {
202 if (!type.isClippable())
207 public void setClipped(boolean c) {
211 fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
214 public boolean isClippedEnabled() {
215 return type.isClippable();
218 public double getShapeParameterMin() {
219 return type.minParameter();
222 public double getShapeParameterMax() {
223 return type.maxParameter();
227 //////// Shoulders ////////
229 public double getForeShoulderRadius() {
230 return foreShoulderRadius;
233 public void setForeShoulderRadius(double foreShoulderRadius) {
234 if (MathUtil.equals(this.foreShoulderRadius, foreShoulderRadius))
236 this.foreShoulderRadius = foreShoulderRadius;
237 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
240 public double getForeShoulderThickness() {
241 return foreShoulderThickness;
244 public void setForeShoulderThickness(double foreShoulderThickness) {
245 if (MathUtil.equals(this.foreShoulderThickness, foreShoulderThickness))
247 this.foreShoulderThickness = foreShoulderThickness;
248 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
251 public double getForeShoulderLength() {
252 return foreShoulderLength;
255 public void setForeShoulderLength(double foreShoulderLength) {
256 if (MathUtil.equals(this.foreShoulderLength, foreShoulderLength))
258 this.foreShoulderLength = foreShoulderLength;
259 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
262 public boolean isForeShoulderCapped() {
263 return foreShoulderCapped;
266 public void setForeShoulderCapped(boolean capped) {
267 if (this.foreShoulderCapped == capped)
269 this.foreShoulderCapped = capped;
270 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
276 public double getAftShoulderRadius() {
277 return aftShoulderRadius;
280 public void setAftShoulderRadius(double aftShoulderRadius) {
281 if (MathUtil.equals(this.aftShoulderRadius, aftShoulderRadius))
283 this.aftShoulderRadius = aftShoulderRadius;
284 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
287 public double getAftShoulderThickness() {
288 return aftShoulderThickness;
291 public void setAftShoulderThickness(double aftShoulderThickness) {
292 if (MathUtil.equals(this.aftShoulderThickness, aftShoulderThickness))
294 this.aftShoulderThickness = aftShoulderThickness;
295 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
298 public double getAftShoulderLength() {
299 return aftShoulderLength;
302 public void setAftShoulderLength(double aftShoulderLength) {
303 if (MathUtil.equals(this.aftShoulderLength, aftShoulderLength))
305 this.aftShoulderLength = aftShoulderLength;
306 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
309 public boolean isAftShoulderCapped() {
310 return aftShoulderCapped;
313 public void setAftShoulderCapped(boolean capped) {
314 if (this.aftShoulderCapped == capped)
316 this.aftShoulderCapped = capped;
317 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
323 /////////// Shape implementations ////////////
328 * Return the radius at point x of the transition.
331 public double getRadius(double x) {
332 if (x < 0 || x > length)
335 double r1 = getForeRadius();
336 double r2 = getAftRadius();
349 // Check clip calculation
351 calculateClip(r1, r2);
352 return type.getRadius(clipLength + x, r2, clipLength + length, shapeParameter);
355 return r1 + type.getRadius(x, r2 - r1, length, shapeParameter);
360 * Numerically solve clipLength from the equation
361 * r1 == type.getRadius(clipLength,r2,clipLength+length)
362 * using a binary search. It assumes getOuterRadius() to be monotonically increasing.
364 private void calculateClip(double r1, double r2) {
365 double min = 0, max = length;
384 // getR(min,min+length,r2) - r1 < 0
385 // getR(max,max+length,r2) - r1 > 0
388 while (type.getRadius(max, r2, max + length, shapeParameter) - r1 < 0) {
397 clipLength = (min + max) / 2;
398 if ((max - min) < CLIP_PRECISION)
400 double val = type.getRadius(clipLength, r2, clipLength + length, shapeParameter);
411 public double getInnerRadius(double x) {
412 return Math.max(getRadius(x) - thickness, 0);
418 public Collection<Coordinate> getComponentBounds() {
419 Collection<Coordinate> bounds = super.getComponentBounds();
420 if (foreShoulderLength > 0.001)
421 addBound(bounds, -foreShoulderLength, foreShoulderRadius);
422 if (aftShoulderLength > 0.001)
423 addBound(bounds, getLength() + aftShoulderLength, aftShoulderRadius);
428 public double getComponentMass() {
429 double mass = super.getComponentMass();
430 if (getForeShoulderLength() > 0.001) {
431 final double or = getForeShoulderRadius();
432 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
433 mass += ringMass(or, ir, getForeShoulderLength(), getMaterial().getDensity());
435 if (isForeShoulderCapped()) {
436 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
437 mass += ringMass(ir, 0, getForeShoulderThickness(), getMaterial().getDensity());
440 if (getAftShoulderLength() > 0.001) {
441 final double or = getAftShoulderRadius();
442 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
443 mass += ringMass(or, ir, getAftShoulderLength(), getMaterial().getDensity());
445 if (isAftShoulderCapped()) {
446 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
447 mass += ringMass(ir, 0, getAftShoulderThickness(), getMaterial().getDensity());
454 public Coordinate getComponentCG() {
455 Coordinate cg = super.getComponentCG();
456 if (getForeShoulderLength() > 0.001) {
457 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
458 cg = cg.average(ringCG(getForeShoulderRadius(), ir, -getForeShoulderLength(), 0,
459 getMaterial().getDensity()));
461 if (isForeShoulderCapped()) {
462 final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
463 cg = cg.average(ringCG(ir, 0, -getForeShoulderLength(),
464 getForeShoulderThickness() - getForeShoulderLength(),
465 getMaterial().getDensity()));
468 if (getAftShoulderLength() > 0.001) {
469 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
470 cg = cg.average(ringCG(getAftShoulderRadius(), ir, getLength(),
471 getLength() + getAftShoulderLength(), getMaterial().getDensity()));
473 if (isAftShoulderCapped()) {
474 final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
475 cg = cg.average(ringCG(ir, 0,
476 getLength() + getAftShoulderLength() - getAftShoulderThickness(),
477 getLength() + getAftShoulderLength(), getMaterial().getDensity()));
484 * The moments of inertia are not explicitly corrected for the shoulders.
485 * However, since the mass is corrected, the inertia is automatically corrected
486 * to very nearly the correct value.
492 * Returns the name of the component ("Transition").
495 public String getComponentName() {
497 return trans.get("Transition.Transition");
501 protected void componentChanged(ComponentChangeEvent e) {
502 super.componentChanged(e);
507 * Accept a visitor to this Transition in the component hierarchy.
509 * @param theVisitor the visitor that will be called back with a reference to this Transition
512 public void accept (final ComponentVisitor theVisitor) {
513 theVisitor.visit(this);
517 * Check whether the given type can be added to this component. Transitions allow any
518 * InternalComponents to be added.
520 * @param type The RocketComponent class type to add.
521 * @return Whether such a component can be added.
524 public boolean isCompatible(Class<? extends RocketComponent> type) {
525 if (InternalComponent.class.isAssignableFrom(type))
533 * An enumeration listing the possible shapes of transitions.
535 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
537 public static enum Shape {
543 CONICAL(trans.get("Shape.Conical"),
544 //// A conical nose cone has a profile of a triangle.
545 trans.get("Shape.Conical.desc1"),
546 //// A conical transition has straight sides.
547 trans.get("Shape.Conical.desc2")) {
549 public double getRadius(double x, double radius, double length, double param) {
553 return radius * x / length;
558 * Ogive shape. The shape parameter is the portion of an extended tangent ogive
559 * that will be used. That is, for param==1 a tangent ogive will be produced, and
560 * for smaller values the shape straightens out into a cone at param==0.
563 OGIVE(trans.get("Shape.Ogive"),
564 //// 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>.
565 trans.get("Shape.Ogive.desc1"),
566 //// 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>.
567 trans.get("Shape.Ogive.desc2")) {
569 public boolean usesParameter() {
570 return true; // Range 0...1 is default
574 public double defaultParameter() {
575 return 1.0; // Tangent ogive by default
579 public double getRadius(double x, double radius, double length, double param) {
586 // Impossible to calculate ogive for length < radius, scale instead
587 // TODO: LOW: secant ogive could be calculated lower
588 if (length < radius) {
589 x = x * radius / length;
594 return CONICAL.getRadius(x, radius, length, param);
596 // Radius of circle is:
597 double R = sqrt((pow2(length) + pow2(radius)) *
598 (pow2((2 - param) * length) + pow2(param * radius)) / (4 * pow2(param * radius)));
599 double L = length / param;
600 // double R = (radius + length*length/(radius*param*param))/2;
601 double y0 = sqrt(R * R - L * L);
602 return sqrt(R * R - (L - x) * (L - x)) - y0;
610 ELLIPSOID(trans.get("Shape.Ellipsoid"),
611 //// An ellipsoidal nose cone has a profile of a half-ellipse with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>.
612 trans.get("Shape.Ellipsoid.desc1"),
613 //// 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.
614 trans.get("Shape.Ellipsoid.desc2"), true) {
616 public double getRadius(double x, double radius, double length, double param) {
620 x = x * radius / length;
621 return sqrt(2 * radius * x - x * x); // radius/length * sphere
625 POWER(trans.get("Shape.Powerseries"),
626 //// A power series nose cone has a profile of <i>Radius</i> × (<i>x</i> / <i>Length</i>)<sup><i>k</i></sup> where <i>k</i> is the shape parameter. For <i>k</i>=0.5 this is a <b>
627 trans.get("Shape.Powerseries.descA1") + FRAC12 +
628 //// -power</b> or <b>parabolic</b> nose cone, for <i>k</i>=0.75 a <b>
629 trans.get("Shape.Powerseries.descA2") + FRAC34 +
630 //// -power</b>, and for <i>k</i>=1 a <b>conical</b> nose cone.
631 trans.get("Shape.Powerseries.descA3"),
632 //// A power series transition has a profile of <i>Radius</i> × (<i>x</i> / <i>Length</i>)<sup><i>k</i></sup> where <i>k</i> is the shape parameter. For <i>k</i>=0.5 the transition is <b>
633 trans.get("Shape.Powerseries.descB1") + FRAC12 +
634 //// -power</b> or <b>parabolic</b>, for <i>k</i>=0.75 a <b>
635 trans.get("Shape.Powerseries.descB2") + FRAC34 +
636 //// -power</b>, and for <i>k</i>=1 <b>conical</b>.
637 trans.get("Shape.Powerseries.descB3"), true) {
639 public boolean usesParameter() { // Range 0...1
644 public double defaultParameter() {
649 public double getRadius(double x, double radius, double length, double param) {
655 if (param <= 0.00001) {
661 return radius * Math.pow(x / length, param);
665 //// Parabolic series
666 PARABOLIC(trans.get("Shape.Parabolicseries"),
667 ////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.
668 trans.get("Shape.Parabolicseries.desc1"),
669 ////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.
670 trans.get("Shape.Parabolicseries.desc2")) {
672 // In principle a parabolic transition is clippable, but the difference is
676 public boolean usesParameter() { // Range 0...1
681 public double defaultParameter() {
686 public double getRadius(double x, double radius, double length, double param) {
693 return radius * ((2 * x / length - param * pow2(x / length)) / (2 - param));
699 HAACK(trans.get("Shape.Haackseries"),
700 //// 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.
701 trans.get("Shape.Haackseries.desc1"),
702 //// 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.
703 trans.get("Shape.Haackseries.desc2"), true) {
705 public boolean usesParameter() {
710 public double maxParameter() {
711 return 1.0 / 3.0; // Range 0...1/3
715 public double getRadius(double x, double radius, double length, double param) {
722 double theta = Math.acos(1 - 2 * x / length);
724 return radius * sqrt((theta - sin(2 * theta) / 2) / Math.PI);
726 return radius * sqrt((theta - sin(2 * theta) / 2 + param * pow3(sin(theta))) / Math.PI);
730 // POLYNOMIAL("Smooth polynomial",
731 // "A polynomial is fitted such that the nose cone profile is horizontal "+
732 // "at the aft end of the transition. The angle at the tip is defined by "+
733 // "the shape parameter.",
734 // "A polynomial is fitted such that the transition profile is horizontal "+
735 // "at the aft end of the transition. The angle at the fore end is defined "+
736 // "by the shape parameter.") {
738 // public boolean usesParameter() {
742 // public double maxParameter() {
743 // return 3.0; // Range 0...3
746 // public double defaultParameter() {
749 // public double getRadius(double x, double radius, double length, double param) {
751 // assert x <= length;
752 // assert radius >= 0;
753 // assert param >= 0;
754 // assert param <= 3;
755 // // p(x) = (k-2)x^3 + (3-2k)x^2 + k*x
757 // return radius*((((param-2)*x + (3-2*param))*x + param)*x);
762 // Privete fields of the shapes
763 private final String name;
764 private final String transitionDesc;
765 private final String noseconeDesc;
766 private final boolean canClip;
768 // Non-clippable constructor
769 Shape(String name, String noseconeDesc, String transitionDesc) {
770 this(name, noseconeDesc, transitionDesc, false);
773 // Clippable constructor
774 Shape(String name, String noseconeDesc, String transitionDesc, boolean canClip) {
776 this.canClip = canClip;
777 this.noseconeDesc = noseconeDesc;
778 this.transitionDesc = transitionDesc;
783 * Return the name of the transition shape name.
785 public String getName() {
790 * Get a description of the Transition shape.
792 public String getTransitionDescription() {
793 return transitionDesc;
797 * Get a description of the NoseCone shape.
799 public String getNoseConeDescription() {
804 * Check whether the shape differs in clipped mode. The clipping should be
805 * enabled by default if possible.
807 public boolean isClippable() {
812 * Return whether the shape uses the shape parameter. (Default false.)
814 public boolean usesParameter() {
819 * Return the minimum value of the shape parameter. (Default 0.)
821 public double minParameter() {
826 * Return the maximum value of the shape parameter. (Default 1.)
828 public double maxParameter() {
833 * Return the default value of the shape parameter. (Default 0.)
835 public double defaultParameter() {
840 * Calculate the basic radius of a transition with the given radius, length and
841 * shape parameter at the point x from the tip of the component. It is assumed
842 * that the fore radius if zero and the aft radius is <code>radius >= 0</code>.
843 * Boattails are achieved by reversing the component.
845 * @param x Position from the tip of the component.
846 * @param radius Aft end radius >= 0.
847 * @param length Length of the transition >= 0.
848 * @param param Valid shape parameter.
849 * @return The basic radius at the given position.
851 public abstract double getRadius(double x, double radius, double length, double param);
855 * Returns the name of the shape (same as getName()).
858 public String toString() {