1 package net.sf.openrocket.rocketcomponent;
3 import static net.sf.openrocket.util.MathUtil.pow2;
5 import java.util.ArrayList;
6 import java.util.Collection;
9 import net.sf.openrocket.util.Coordinate;
10 import net.sf.openrocket.util.MathUtil;
14 * Class for an axially symmetric rocket component generated by rotating
15 * a function y=f(x) >= 0 around the x-axis (eg. tube, cone, etc.)
17 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
20 public abstract class SymmetricComponent extends BodyComponent implements RadialParent {
21 public static final double DEFAULT_RADIUS = 0.025;
22 public static final double DEFAULT_THICKNESS = 0.002;
24 private static final int DIVISIONS = 100; // No. of divisions when integrating
26 protected boolean filled = false;
27 protected double thickness = DEFAULT_THICKNESS;
30 // Cached data, default values signify not calculated
31 private double wetArea = -1;
32 private double planArea = -1;
33 private double planCenter = -1;
34 private double volume = -1;
35 private double fullVolume = -1;
36 private double longitudalInertia = -1;
37 private double rotationalInertia = -1;
38 private Coordinate cg = null;
42 public SymmetricComponent() {
48 * Return the component radius at position x.
49 * @param x Position on x-axis.
50 * @return Radius of the component at the given position, or 0 if outside
53 public abstract double getRadius(double x);
54 public abstract double getInnerRadius(double x);
56 public abstract double getForeRadius();
57 public abstract boolean isForeRadiusAutomatic();
58 public abstract double getAftRadius();
59 public abstract boolean isAftRadiusAutomatic();
62 // Implement the Radial interface:
63 public final double getOuterRadius(double x) {
69 public final double getRadius(double x, double theta) {
74 public final double getInnerRadius(double x, double theta) {
75 return getInnerRadius(x);
81 * Return the component wall thickness.
83 public double getThickness() {
85 return Math.max(getForeRadius(),getAftRadius());
86 return Math.min(thickness,Math.max(getForeRadius(),getAftRadius()));
91 * Set the component wall thickness. Values greater than the maximum radius are not
92 * allowed, and will result in setting the thickness to the maximum radius.
94 public void setThickness(double thickness) {
95 if ((this.thickness == thickness) && !filled)
97 this.thickness = MathUtil.clamp(thickness,0,Math.max(getForeRadius(),getAftRadius()));
99 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
104 * Returns whether the component is set as filled. If it is set filled, then the
105 * wall thickness will have no effect.
107 public boolean isFilled() {
113 * Sets whether the component is set as filled. If the component is filled, then
114 * the wall thickness will have no effect.
116 public void setFilled(boolean filled) {
117 if (this.filled == filled)
119 this.filled = filled;
120 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
125 * Adds component bounds at a number of points between 0...length.
128 public Collection<Coordinate> getComponentBounds() {
129 List<Coordinate> list = new ArrayList<Coordinate>(20);
130 for (int n=0; n<=5; n++) {
131 double x = n*length/5;
132 double r = getRadius(x);
141 * Calculate volume of the component by integrating over the length of the component.
142 * The method caches the result, so subsequent calls are instant. Subclasses may
143 * override this method for simple shapes and use this method as necessary.
145 * @return The volume of the component.
148 public double getComponentVolume() {
156 * Calculate full (filled) volume of the component by integrating over the length
157 * of the component. The method caches the result, so subsequent calls are instant.
158 * Subclasses may override this method for simple shapes and use this method as
161 * @return The filled volume of the component.
163 public double getFullVolume() {
171 * Calculate the wetted area of the component by integrating over the length
172 * of the component. The method caches the result, so subsequent calls are instant.
173 * Subclasses may override this method for simple shapes and use this method as
176 * @return The wetted area of the component.
178 public double getComponentWetArea() {
186 * Calculate the planform area of the component by integrating over the length of
187 * the component. The method caches the result, so subsequent calls are instant.
188 * Subclasses may override this method for simple shapes and use this method as
191 * @return The planform area of the component.
193 public double getComponentPlanformArea() {
201 * Calculate the planform center X-coordinate of the component by integrating over
202 * the length of the component. The planform center is defined as
203 * <pre> integrate(x*2*r(x)) / planform area </pre>
204 * The method caches the result, so subsequent calls are instant. Subclasses may
205 * override this method for simple shapes and use this method as necessary.
207 * @return The planform center of the component.
209 public double getComponentPlanformCenter() {
217 * Calculate CG of the component by integrating over the length of the component.
218 * The method caches the result, so subsequent calls are instant. Subclasses may
219 * override this method for simple shapes and use this method as necessary.
221 * @return The CG+mass of the component.
224 public Coordinate getComponentCG() {
232 public double getLongitudalUnitInertia() {
233 if (longitudalInertia < 0) {
234 if (getComponentVolume() > 0.0000001) // == 0.1cm^3
235 integrateInertiaVolume();
237 integrateInertiaSurface();
239 return longitudalInertia;
244 public double getRotationalUnitInertia() {
245 if (rotationalInertia < 0) {
246 if (getComponentVolume() > 0.0000001)
247 integrateInertiaVolume();
249 integrateInertiaSurface();
251 return rotationalInertia;
257 * Performs integration over the length of the component and updates the cached variables.
259 private void integrate() {
274 // Integrate for volume, CG, wetted area and planform area
276 final double l = length/DIVISIONS;
277 final double pil = Math.PI*l; // PI * l
278 final double pil3 = Math.PI*l/3; // PI * l/3
288 for (int n=1; n<=DIVISIONS; n++) {
290 * r1 and r2 are the two radii
291 * x is the position of r1
292 * hyp is the length of the hypotenuse from r1 to r2
293 * height if the y-axis height of the component if not filled
297 final double hyp = MathUtil.hypot(r2-r1, l);
300 // Volume differential elements
304 dFullV = pil3*(r1*r1 + r1*r2 + r2*r2);
305 if (filled || r1<thickness || r2<thickness) {
310 final double height = thickness*hyp/l;
311 dV = MathUtil.max(pil*height*(r1+r2-height), 0);
314 // Add to the volume-related components
316 fullVolume += dFullV;
319 // Wetted area ( * PI at the end)
320 wetArea += hyp*(r1+r2);
322 // Planform area & center
323 final double p = l*(r1+r2);
325 planCenter += (x+l/2)*p;
327 // Update for next iteration
335 planCenter /= planArea;
337 if (volume < 0.0000000001) { // 0.1 mm^3
339 cg = new Coordinate(length/2, 0, 0, 0);
341 // getComponentMass is safe now
342 // Use super.getComponentMass() to ensure only the transition shape mass
343 // is used, not the shoulders
344 cg = new Coordinate(cgx/volume,0,0,super.getComponentMass());
350 * Integrate the longitudal and rotational inertia based on component volume.
351 * This method may be used only if the total volume is zero.
353 private void integrateInertiaVolume() {
356 final double l = length/DIVISIONS;
357 final double pil = Math.PI*l; // PI * l
358 final double pil3 = Math.PI*l/3; // PI * l/3
362 longitudalInertia = 0;
363 rotationalInertia = 0;
367 for (int n=1; n<=DIVISIONS; n++) {
369 * r1 and r2 are the two radii, outer is their average
370 * x is the position of r1
371 * hyp is the length of the hypotenuse from r1 to r2
372 * height if the y-axis height of the component if not filled
375 final double outer = (r1 + r2)/2;
378 // Volume differential elements
382 if (filled || r1<thickness || r2<thickness) {
384 dV = pil3*(r1*r1 + r1*r2 + r2*r2);
386 final double hyp = MathUtil.hypot(r2-r1, l);
387 final double height = thickness*hyp/l;
388 dV = pil*height*(r1+r2-height);
389 inner = Math.max(outer-height, 0);
392 rotationalInertia += dV * (pow2(outer) + pow2(inner))/2;
393 longitudalInertia += dV * ((3 * (pow2(outer) + pow2(inner)) + pow2(l))/12
398 // Update for next iteration
403 if (MathUtil.equals(volume,0)) {
404 integrateInertiaSurface();
408 rotationalInertia /= volume;
409 longitudalInertia /= volume;
411 // Shift longitudal inertia to CG
412 longitudalInertia = Math.max(longitudalInertia - pow2(getComponentCG().x), 0);
418 * Integrate the longitudal and rotational inertia based on component surface area.
419 * This method may be used only if the total volume is zero.
421 private void integrateInertiaSurface() {
424 final double l = length/DIVISIONS;
428 longitudalInertia = 0;
429 rotationalInertia = 0;
433 for (int n=1; n<=DIVISIONS; n++) {
435 * r1 and r2 are the two radii, outer is their average
436 * x is the position of r1
437 * hyp is the length of the hypotenuse from r1 to r2
438 * height if the y-axis height of the component if not filled
441 final double hyp = MathUtil.hypot(r2-r1, l);
442 final double outer = (r1 + r2)/2;
444 final double dS = hyp * (r1+r2) * Math.PI;
446 rotationalInertia += dS * pow2(outer);
447 longitudalInertia += dS * ((6 * pow2(outer) + pow2(l))/12 + pow2(x+l/2));
451 // Update for next iteration
456 if (MathUtil.equals(surface, 0)) {
457 longitudalInertia = 0;
458 rotationalInertia = 0;
462 longitudalInertia /= surface;
463 rotationalInertia /= surface;
465 // Shift longitudal inertia to CG
466 longitudalInertia = Math.max(longitudalInertia - pow2(getComponentCG().x), 0);
473 * Invalidates the cached volume and CG information.
476 protected void componentChanged(ComponentChangeEvent e) {
477 super.componentChanged(e);
478 if (!e.isOtherChange()) {
484 longitudalInertia = -1;
485 rotationalInertia = -1;
492 /////////// Auto radius helper methods
496 * Returns the automatic radius for this component towards the
497 * front of the rocket. The automatics will not search towards the
498 * rear of the rocket for a suitable radius. A positive return value
499 * indicates a preferred radius, a negative value indicates that a
500 * match was not found.
502 protected abstract double getFrontAutoRadius();
505 * Returns the automatic radius for this component towards the
506 * end of the rocket. The automatics will not search towards the
507 * front of the rocket for a suitable radius. A positive return value
508 * indicates a preferred radius, a negative value indicates that a
509 * match was not found.
511 protected abstract double getRearAutoRadius();
516 * Return the previous symmetric component, or null if none exists.
517 * NOTE: This method currently assumes that there are no external
520 * @return the previous SymmetricComponent, or null.
522 protected final SymmetricComponent getPreviousSymmetricComponent() {
524 for (c = this.getPreviousComponent(); c != null; c = c.getPreviousComponent()) {
525 if (c instanceof SymmetricComponent) {
526 return (SymmetricComponent)c;
528 if (!(c instanceof Stage) &&
529 (c.relativePosition == RocketComponent.Position.AFTER))
530 return null; // Bad component type as "parent"
536 * Return the next symmetric component, or null if none exists.
537 * NOTE: This method currently assumes that there are no external
540 * @return the next SymmetricComponent, or null.
542 protected final SymmetricComponent getNextSymmetricComponent() {
544 for (c = this.getNextComponent(); c != null; c = c.getNextComponent()) {
545 if (c instanceof SymmetricComponent) {
546 return (SymmetricComponent)c;
548 if (!(c instanceof Stage) &&
549 (c.relativePosition == RocketComponent.Position.AFTER))
550 return null; // Bad component type as "parent"