package net.sf.openrocket.rocketcomponent; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.preset.ComponentPreset; import net.sf.openrocket.preset.ComponentPreset.Type; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.MathUtil; import java.util.Collection; import static java.lang.Math.sin; import static net.sf.openrocket.util.MathUtil.pow2; import static net.sf.openrocket.util.MathUtil.pow3; public class Transition extends SymmetricComponent { private static final Translator trans = Application.getTranslator(); private static final double CLIP_PRECISION = 0.0001; private Shape type; private double shapeParameter; private boolean clipped; // Not to be read - use isClipped(), which may be overriden private double radius1, radius2; private boolean autoRadius1, autoRadius2; // Whether the start radius is automatic private double foreShoulderRadius; private double foreShoulderThickness; private double foreShoulderLength; private boolean foreShoulderCapped; private double aftShoulderRadius; private double aftShoulderThickness; private double aftShoulderLength; private boolean aftShoulderCapped; // Used to cache the clip length private double clipLength = -1; public Transition() { super(); this.radius1 = DEFAULT_RADIUS; this.radius2 = DEFAULT_RADIUS; this.length = DEFAULT_RADIUS * 3; this.autoRadius1 = true; this.autoRadius2 = true; this.type = Shape.CONICAL; this.shapeParameter = 0; this.clipped = true; } //////// Length //////// @Override public void setLength( double length ) { if ( this.length == length ) { return; } // Need to clearPreset when length changes. clearPreset(); super.setLength( length ); } //////// Fore radius //////// @Override public double getForeRadius() { if (isForeRadiusAutomatic()) { // Get the automatic radius from the front double r = -1; SymmetricComponent c = this.getPreviousSymmetricComponent(); if (c != null) { r = c.getFrontAutoRadius(); } if (r < 0) r = DEFAULT_RADIUS; return r; } return radius1; } public void setForeRadius(double radius) { if ((this.radius1 == radius) && (autoRadius1 == false)) return; this.autoRadius1 = false; this.radius1 = Math.max(radius, 0); if (this.thickness > this.radius1 && this.thickness > this.radius2) this.thickness = Math.max(this.radius1, this.radius2); clearPreset(); fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } @Override public boolean isForeRadiusAutomatic() { return autoRadius1; } public void setForeRadiusAutomatic(boolean auto) { if (autoRadius1 == auto) return; autoRadius1 = auto; clearPreset(); fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } //////// Aft radius ///////// @Override public double getAftRadius() { if (isAftRadiusAutomatic()) { // Return the auto radius from the rear double r = -1; SymmetricComponent c = this.getNextSymmetricComponent(); if (c != null) { r = c.getRearAutoRadius(); } if (r < 0) r = DEFAULT_RADIUS; return r; } return radius2; } public void setAftRadius(double radius) { if ((this.radius2 == radius) && (autoRadius2 == false)) return; this.autoRadius2 = false; this.radius2 = Math.max(radius, 0); if (this.thickness > this.radius1 && this.thickness > this.radius2) this.thickness = Math.max(this.radius1, this.radius2); clearPreset(); fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } @Override public boolean isAftRadiusAutomatic() { return autoRadius2; } public void setAftRadiusAutomatic(boolean auto) { if (autoRadius2 == auto) return; autoRadius2 = auto; clearPreset(); fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } //// Radius automatics @Override protected double getFrontAutoRadius() { if (isAftRadiusAutomatic()) return -1; return getAftRadius(); } @Override protected double getRearAutoRadius() { if (isForeRadiusAutomatic()) return -1; return getForeRadius(); } //////// Type & shape ///////// public Shape getType() { return type; } public void setType(Shape type) { if (type == null) { throw new IllegalArgumentException("setType called with null argument"); } if (this.type == type) return; this.type = type; this.clipped = type.isClippable(); this.shapeParameter = type.defaultParameter(); fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } public double getShapeParameter() { return shapeParameter; } public void setShapeParameter(double n) { if (shapeParameter == n) return; this.shapeParameter = MathUtil.clamp(n, type.minParameter(), type.maxParameter()); fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } public boolean isClipped() { if (!type.isClippable()) return false; return clipped; } public void setClipped(boolean c) { if (clipped == c) return; clipped = c; fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } public boolean isClippedEnabled() { return type.isClippable(); } public double getShapeParameterMin() { return type.minParameter(); } public double getShapeParameterMax() { return type.maxParameter(); } //////// Shoulders //////// public double getForeShoulderRadius() { return foreShoulderRadius; } public void setForeShoulderRadius(double foreShoulderRadius) { if (MathUtil.equals(this.foreShoulderRadius, foreShoulderRadius)) return; this.foreShoulderRadius = foreShoulderRadius; clearPreset(); fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } public double getForeShoulderThickness() { return foreShoulderThickness; } public void setForeShoulderThickness(double foreShoulderThickness) { if (MathUtil.equals(this.foreShoulderThickness, foreShoulderThickness)) return; this.foreShoulderThickness = foreShoulderThickness; fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } public double getForeShoulderLength() { return foreShoulderLength; } public void setForeShoulderLength(double foreShoulderLength) { if (MathUtil.equals(this.foreShoulderLength, foreShoulderLength)) return; this.foreShoulderLength = foreShoulderLength; fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } public boolean isForeShoulderCapped() { return foreShoulderCapped; } public void setForeShoulderCapped(boolean capped) { if (this.foreShoulderCapped == capped) return; this.foreShoulderCapped = capped; fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } public double getAftShoulderRadius() { return aftShoulderRadius; } public void setAftShoulderRadius(double aftShoulderRadius) { if (MathUtil.equals(this.aftShoulderRadius, aftShoulderRadius)) return; this.aftShoulderRadius = aftShoulderRadius; clearPreset(); fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } public double getAftShoulderThickness() { return aftShoulderThickness; } public void setAftShoulderThickness(double aftShoulderThickness) { if (MathUtil.equals(this.aftShoulderThickness, aftShoulderThickness)) return; this.aftShoulderThickness = aftShoulderThickness; fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } public double getAftShoulderLength() { return aftShoulderLength; } public void setAftShoulderLength(double aftShoulderLength) { if (MathUtil.equals(this.aftShoulderLength, aftShoulderLength)) return; this.aftShoulderLength = aftShoulderLength; fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } public boolean isAftShoulderCapped() { return aftShoulderCapped; } public void setAftShoulderCapped(boolean capped) { if (this.aftShoulderCapped == capped) return; this.aftShoulderCapped = capped; fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } /////////// Shape implementations //////////// /** * Return the radius at point x of the transition. */ @Override public double getRadius(double x) { if (x < 0 || x > length) return 0; double r1 = getForeRadius(); double r2 = getAftRadius(); if (r1 == r2) return r1; if (r1 > r2) { x = length - x; double tmp = r1; r1 = r2; r2 = tmp; } if (isClipped()) { // Check clip calculation if (clipLength < 0) calculateClip(r1, r2); return type.getRadius(clipLength + x, r2, clipLength + length, shapeParameter); } else { // Not clipped return r1 + type.getRadius(x, r2 - r1, length, shapeParameter); } } /** * Numerically solve clipLength from the equation * r1 == type.getRadius(clipLength,r2,clipLength+length) * using a binary search. It assumes getOuterRadius() to be monotonically increasing. */ private void calculateClip(double r1, double r2) { double min = 0, max = length; if (r1 >= r2) { double tmp = r1; r1 = r2; r2 = tmp; } if (r1 == 0) { clipLength = 0; return; } if (length <= 0) { clipLength = 0; return; } // Required: // getR(min,min+length,r2) - r1 < 0 // getR(max,max+length,r2) - r1 > 0 int n = 0; while (type.getRadius(max, r2, max + length, shapeParameter) - r1 < 0) { min = max; max *= 2; n++; if (n > 10) break; } while (true) { clipLength = (min + max) / 2; if ((max - min) < CLIP_PRECISION) return; double val = type.getRadius(clipLength, r2, clipLength + length, shapeParameter); if (val - r1 > 0) { max = clipLength; } else { min = clipLength; } } } @Override public double getInnerRadius(double x) { return Math.max(getRadius(x) - thickness, 0); } @Override public Collection<Coordinate> getComponentBounds() { Collection<Coordinate> bounds = super.getComponentBounds(); if (foreShoulderLength > 0.001) addBound(bounds, -foreShoulderLength, foreShoulderRadius); if (aftShoulderLength > 0.001) addBound(bounds, getLength() + aftShoulderLength, aftShoulderRadius); return bounds; } @Override public double getComponentVolume() { double volume = super.getComponentVolume(); if (getForeShoulderLength() > 0.001) { final double or = getForeShoulderRadius(); final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0); volume += ringVolume( or, ir, getForeShoulderLength() ); } if (isForeShoulderCapped()) { final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0); volume += ringVolume(ir, 0, getForeShoulderThickness() ); } if (getAftShoulderLength() > 0.001) { final double or = getAftShoulderRadius(); final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0); volume += ringVolume(or, ir, getAftShoulderLength() ); } if (isAftShoulderCapped()) { final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0); volume += ringVolume(ir, 0, getAftShoulderThickness() ); } return volume; } @Override public Coordinate getComponentCG() { Coordinate cg = super.getComponentCG(); if (getForeShoulderLength() > 0.001) { final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0); cg = cg.average(ringCG(getForeShoulderRadius(), ir, -getForeShoulderLength(), 0, getMaterial().getDensity())); } if (isForeShoulderCapped()) { final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0); cg = cg.average(ringCG(ir, 0, -getForeShoulderLength(), getForeShoulderThickness() - getForeShoulderLength(), getMaterial().getDensity())); } if (getAftShoulderLength() > 0.001) { final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0); cg = cg.average(ringCG(getAftShoulderRadius(), ir, getLength(), getLength() + getAftShoulderLength(), getMaterial().getDensity())); } if (isAftShoulderCapped()) { final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0); cg = cg.average(ringCG(ir, 0, getLength() + getAftShoulderLength() - getAftShoulderThickness(), getLength() + getAftShoulderLength(), getMaterial().getDensity())); } return cg; } /* * The moments of inertia are not explicitly corrected for the shoulders. * However, since the mass is corrected, the inertia is automatically corrected * to very nearly the correct value. */ /** * Returns the name of the component ("Transition"). */ @Override public String getComponentName() { //// Transition return trans.get("Transition.Transition"); } @Override protected void componentChanged(ComponentChangeEvent e) { super.componentChanged(e); clipLength = -1; } /** * Check whether the given type can be added to this component. Transitions allow any * InternalComponents to be added. * * @param ctype The RocketComponent class type to add. * @return Whether such a component can be added. */ @Override public boolean isCompatible(Class<? extends RocketComponent> ctype) { if (InternalComponent.class.isAssignableFrom(ctype)) return true; return false; } @Override public Type getPresetType() { return ComponentPreset.Type.TRANSITION; } @Override protected void loadFromPreset(ComponentPreset preset) { boolean presetFilled = false; if ( preset.has(ComponentPreset.FILLED ) ) { presetFilled = preset.get( ComponentPreset.FILLED); } if ( preset.has(ComponentPreset.SHAPE) ) { Shape s = preset.get(ComponentPreset.SHAPE); this.setType(s); } if ( preset.has(ComponentPreset.AFT_OUTER_DIAMETER) ) { double outerDiameter = preset.get(ComponentPreset.AFT_OUTER_DIAMETER); this.setAftRadiusAutomatic(false); this.setAftRadius(outerDiameter/2.0); } if ( preset.has(ComponentPreset.AFT_SHOULDER_LENGTH) ) { double d = preset.get(ComponentPreset.AFT_SHOULDER_LENGTH); this.setAftShoulderLength(d); } if ( preset.has(ComponentPreset.AFT_SHOULDER_DIAMETER) ) { double d = preset.get(ComponentPreset.AFT_SHOULDER_DIAMETER); this.setAftShoulderRadius(d/2.0); if ( presetFilled ) { this.setAftShoulderThickness(d/2.0); } } if ( preset.has(ComponentPreset.FORE_OUTER_DIAMETER) ) { double outerDiameter = preset.get(ComponentPreset.FORE_OUTER_DIAMETER); this.setForeRadiusAutomatic(false); this.setForeRadius(outerDiameter/2.0); } if ( preset.has(ComponentPreset.FORE_SHOULDER_LENGTH) ) { double d = preset.get(ComponentPreset.FORE_SHOULDER_LENGTH); this.setForeShoulderLength(d); } if ( preset.has(ComponentPreset.FORE_SHOULDER_DIAMETER) ) { double d = preset.get(ComponentPreset.FORE_SHOULDER_DIAMETER); this.setForeShoulderRadius(d/2.0); if ( presetFilled ) { this.setForeShoulderThickness(d/2.0); } } super.loadFromPreset(preset); fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } /** * An enumeration listing the possible shapes of transitions. * * @author Sampo Niskanen <sampo.niskanen@iki.fi> */ public static enum Shape { /** * Conical shape. */ //// Conical CONICAL(trans.get("Shape.Conical"), //// A conical nose cone has a profile of a triangle. trans.get("Shape.Conical.desc1"), //// A conical transition has straight sides. trans.get("Shape.Conical.desc2")) { @Override public double getRadius(double x, double radius, double length, double param) { assert x >= 0; assert x <= length; assert radius >= 0; return radius * x / length; } }, /** * Ogive shape. The shape parameter is the portion of an extended tangent ogive * that will be used. That is, for param==1 a tangent ogive will be produced, and * for smaller values the shape straightens out into a cone at param==0. */ //// Ogive OGIVE(trans.get("Shape.Ogive"), //// 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>. trans.get("Shape.Ogive.desc1"), //// 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>. trans.get("Shape.Ogive.desc2")) { @Override public boolean usesParameter() { return true; // Range 0...1 is default } @Override public double defaultParameter() { return 1.0; // Tangent ogive by default } @Override public double getRadius(double x, double radius, double length, double param) { assert x >= 0; assert x <= length; assert radius >= 0; assert param >= 0; assert param <= 1; // Impossible to calculate ogive for length < radius, scale instead // TODO: LOW: secant ogive could be calculated lower if (length < radius) { x = x * radius / length; length = radius; } if (param < 0.001) return CONICAL.getRadius(x, radius, length, param); // Radius of circle is: double R = MathUtil.safeSqrt((pow2(length) + pow2(radius)) * (pow2((2 - param) * length) + pow2(param * radius)) / (4 * pow2(param * radius))); double L = length / param; // double R = (radius + length*length/(radius*param*param))/2; double y0 = MathUtil.safeSqrt(R * R - L * L); return MathUtil.safeSqrt(R * R - (L - x) * (L - x)) - y0; } }, /** * Ellipsoidal shape. */ //// Ellipsoid ELLIPSOID(trans.get("Shape.Ellipsoid"), //// An ellipsoidal nose cone has a profile of a half-ellipse with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>. trans.get("Shape.Ellipsoid.desc1"), //// 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. trans.get("Shape.Ellipsoid.desc2"), true) { @Override public double getRadius(double x, double radius, double length, double param) { assert x >= 0; assert x <= length; assert radius >= 0; x = x * radius / length; return MathUtil.safeSqrt(2 * radius * x - x * x); // radius/length * sphere } }, //// Power series POWER(trans.get("Shape.Powerseries"), trans.get("Shape.Powerseries.desc1"), trans.get("Shape.Powerseries.desc2"), true) { @Override public boolean usesParameter() { // Range 0...1 return true; } @Override public double defaultParameter() { return 0.5; } @Override public double getRadius(double x, double radius, double length, double param) { assert x >= 0; assert x <= length; assert radius >= 0; assert param >= 0; assert param <= 1; if (param <= 0.00001) { if (x <= 0.00001) return 0; else return radius; } return radius * Math.pow(x / length, param); } }, //// Parabolic series PARABOLIC(trans.get("Shape.Parabolicseries"), ////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. trans.get("Shape.Parabolicseries.desc1"), ////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. trans.get("Shape.Parabolicseries.desc2")) { // In principle a parabolic transition is clippable, but the difference is // negligible. @Override public boolean usesParameter() { // Range 0...1 return true; } @Override public double defaultParameter() { return 1.0; } @Override public double getRadius(double x, double radius, double length, double param) { assert x >= 0; assert x <= length; assert radius >= 0; assert param >= 0; assert param <= 1; return radius * ((2 * x / length - param * pow2(x / length)) / (2 - param)); } }, //// Haack series HAACK(trans.get("Shape.Haackseries"), //// 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. trans.get("Shape.Haackseries.desc1"), //// 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. trans.get("Shape.Haackseries.desc2"), true) { @Override public boolean usesParameter() { return true; } @Override public double maxParameter() { return 1.0 / 3.0; // Range 0...1/3 } @Override public double getRadius(double x, double radius, double length, double param) { assert x >= 0; assert x <= length; assert radius >= 0; assert param >= 0; assert param <= 2; double theta = Math.acos(1 - 2 * x / length); if (MathUtil.equals(param, 0)) { return radius * MathUtil.safeSqrt((theta - sin(2 * theta) / 2) / Math.PI); } return radius * MathUtil.safeSqrt((theta - sin(2 * theta) / 2 + param * pow3(sin(theta))) / Math.PI); } }, // POLYNOMIAL("Smooth polynomial", // "A polynomial is fitted such that the nose cone profile is horizontal "+ // "at the aft end of the transition. The angle at the tip is defined by "+ // "the shape parameter.", // "A polynomial is fitted such that the transition profile is horizontal "+ // "at the aft end of the transition. The angle at the fore end is defined "+ // "by the shape parameter.") { // @Override // public boolean usesParameter() { // return true; // } // @Override // public double maxParameter() { // return 3.0; // Range 0...3 // } // @Override // public double defaultParameter() { // return 0.0; // } // public double getRadius(double x, double radius, double length, double param) { // assert x >= 0; // assert x <= length; // assert radius >= 0; // assert param >= 0; // assert param <= 3; // // p(x) = (k-2)x^3 + (3-2k)x^2 + k*x // x = x/length; // return radius*((((param-2)*x + (3-2*param))*x + param)*x); // } // } ; // Privete fields of the shapes private final String name; private final String transitionDesc; private final String noseconeDesc; private final boolean canClip; // Non-clippable constructor Shape(String name, String noseconeDesc, String transitionDesc) { this(name, noseconeDesc, transitionDesc, false); } // Clippable constructor Shape(String name, String noseconeDesc, String transitionDesc, boolean canClip) { this.name = name; this.canClip = canClip; this.noseconeDesc = noseconeDesc; this.transitionDesc = transitionDesc; } /** * Return the name of the transition shape name. */ public String getName() { return name; } /** * Get a description of the Transition shape. */ public String getTransitionDescription() { return transitionDesc; } /** * Get a description of the NoseCone shape. */ public String getNoseConeDescription() { return noseconeDesc; } /** * Check whether the shape differs in clipped mode. The clipping should be * enabled by default if possible. */ public boolean isClippable() { return canClip; } /** * Return whether the shape uses the shape parameter. (Default false.) */ public boolean usesParameter() { return false; } /** * Return the minimum value of the shape parameter. (Default 0.) */ public double minParameter() { return 0.0; } /** * Return the maximum value of the shape parameter. (Default 1.) */ public double maxParameter() { return 1.0; } /** * Return the default value of the shape parameter. (Default 0.) */ public double defaultParameter() { return 0.0; } /** * Calculate the basic radius of a transition with the given radius, length and * shape parameter at the point x from the tip of the component. It is assumed * that the fore radius if zero and the aft radius is <code>radius >= 0</code>. * Boattails are achieved by reversing the component. * * @param x Position from the tip of the component. * @param radius Aft end radius >= 0. * @param length Length of the transition >= 0. * @param param Valid shape parameter. * @return The basic radius at the given position. */ public abstract double getRadius(double x, double radius, double length, double param); /** * Returns the name of the shape (same as getName()). */ @Override public String toString() { return name; } /** * Lookup the Shape given the localized name. This differs from the standard valueOf as that looks up * based on the canonical name, not the localized name which is an instance var. * * @param localizedName * @return */ public static Shape toShape(String localizedName) { Shape[] values = Shape.values(); for (Shape value : values) { if (value.getName().equals(localizedName)) { return value; } } return null; } } }