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 = pil*height*(r1+r2-height);
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;
340 // getComponentMass is safe now
341 cg = new Coordinate(cgx/volume,0,0,getComponentMass());
347 * Integrate the longitudal and rotational inertia based on component volume.
348 * This method may be used only if the total volume is zero.
350 private void integrateInertiaVolume() {
353 final double l = length/DIVISIONS;
354 final double pil = Math.PI*l; // PI * l
355 final double pil3 = Math.PI*l/3; // PI * l/3
359 longitudalInertia = 0;
360 rotationalInertia = 0;
364 for (int n=1; n<=DIVISIONS; n++) {
366 * r1 and r2 are the two radii, outer is their average
367 * x is the position of r1
368 * hyp is the length of the hypotenuse from r1 to r2
369 * height if the y-axis height of the component if not filled
372 final double outer = (r1 + r2)/2;
375 // Volume differential elements
379 if (filled || r1<thickness || r2<thickness) {
381 dV = pil3*(r1*r1 + r1*r2 + r2*r2);
383 final double hyp = MathUtil.hypot(r2-r1, l);
384 final double height = thickness*hyp/l;
385 dV = pil*height*(r1+r2-height);
386 inner = Math.max(outer-height, 0);
389 rotationalInertia += dV * (pow2(outer) + pow2(inner))/2;
390 longitudalInertia += dV * ((3 * (pow2(outer) + pow2(inner)) + pow2(l))/12
395 // Update for next iteration
400 if (MathUtil.equals(volume,0)) {
401 integrateInertiaSurface();
405 rotationalInertia /= volume;
406 longitudalInertia /= volume;
408 // Shift longitudal inertia to CG
409 longitudalInertia = Math.max(longitudalInertia - pow2(getComponentCG().x), 0);
415 * Integrate the longitudal and rotational inertia based on component surface area.
416 * This method may be used only if the total volume is zero.
418 private void integrateInertiaSurface() {
421 final double l = length/DIVISIONS;
425 longitudalInertia = 0;
426 rotationalInertia = 0;
430 for (int n=1; n<=DIVISIONS; n++) {
432 * r1 and r2 are the two radii, outer is their average
433 * x is the position of r1
434 * hyp is the length of the hypotenuse from r1 to r2
435 * height if the y-axis height of the component if not filled
438 final double hyp = MathUtil.hypot(r2-r1, l);
439 final double outer = (r1 + r2)/2;
441 final double dS = hyp * (r1+r2) * Math.PI;
443 rotationalInertia += dS * pow2(outer);
444 longitudalInertia += dS * ((6 * pow2(outer) + pow2(l))/12 + pow2(x+l/2));
448 // Update for next iteration
453 if (MathUtil.equals(surface, 0)) {
454 longitudalInertia = 0;
455 rotationalInertia = 0;
459 longitudalInertia /= surface;
460 rotationalInertia /= surface;
462 // Shift longitudal inertia to CG
463 longitudalInertia = Math.max(longitudalInertia - pow2(getComponentCG().x), 0);
470 * Invalidates the cached volume and CG information.
473 protected void componentChanged(ComponentChangeEvent e) {
474 super.componentChanged(e);
475 if (!e.isOtherChange()) {
481 longitudalInertia = -1;
482 rotationalInertia = -1;
489 /////////// Auto radius helper methods
493 * Returns the automatic radius for this component towards the
494 * front of the rocket. The automatics will not search towards the
495 * rear of the rocket for a suitable radius. A positive return value
496 * indicates a preferred radius, a negative value indicates that a
497 * match was not found.
499 protected abstract double getFrontAutoRadius();
502 * Returns the automatic radius for this component towards the
503 * end of the rocket. The automatics will not search towards the
504 * front of the rocket for a suitable radius. A positive return value
505 * indicates a preferred radius, a negative value indicates that a
506 * match was not found.
508 protected abstract double getRearAutoRadius();
513 * Return the previous symmetric component, or null if none exists.
514 * NOTE: This method currently assumes that there are no external
517 * @return the previous SymmetricComponent, or null.
519 protected final SymmetricComponent getPreviousSymmetricComponent() {
521 for (c = this.getPreviousComponent(); c != null; c = c.getPreviousComponent()) {
522 if (c instanceof SymmetricComponent) {
523 return (SymmetricComponent)c;
525 if (!(c instanceof Stage) &&
526 (c.relativePosition == RocketComponent.Position.AFTER))
527 return null; // Bad component type as "parent"
533 * Return the next symmetric component, or null if none exists.
534 * NOTE: This method currently assumes that there are no external
537 * @return the next SymmetricComponent, or null.
539 protected final SymmetricComponent getNextSymmetricComponent() {
541 for (c = this.getNextComponent(); c != null; c = c.getNextComponent()) {
542 if (c instanceof SymmetricComponent) {
543 return (SymmetricComponent)c;
545 if (!(c instanceof Stage) &&
546 (c.relativePosition == RocketComponent.Position.AFTER))
547 return null; // Bad component type as "parent"