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.preset.ComponentPreset;
10 import net.sf.openrocket.util.Coordinate;
11 import net.sf.openrocket.util.MathUtil;
15 * Class for an axially symmetric rocket component generated by rotating
16 * a function y=f(x) >= 0 around the x-axis (eg. tube, cone, etc.)
18 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
21 public abstract class SymmetricComponent extends BodyComponent implements RadialParent {
22 public static final double DEFAULT_RADIUS = 0.025;
23 public static final double DEFAULT_THICKNESS = 0.002;
25 private static final int DIVISIONS = 100; // No. of divisions when integrating
27 protected boolean filled = false;
28 protected double thickness = DEFAULT_THICKNESS;
31 // Cached data, default values signify not calculated
32 private double wetArea = -1;
33 private double planArea = -1;
34 private double planCenter = -1;
35 private double volume = -1;
36 private double fullVolume = -1;
37 private double longitudinalInertia = -1;
38 private double rotationalInertia = -1;
39 private Coordinate cg = null;
43 public SymmetricComponent() {
49 * Return the component radius at position x.
50 * @param x Position on x-axis.
51 * @return Radius of the component at the given position, or 0 if outside
54 public abstract double getRadius(double x);
57 public abstract double getInnerRadius(double x);
59 public abstract double getForeRadius();
61 public abstract boolean isForeRadiusAutomatic();
63 public abstract double getAftRadius();
65 public abstract boolean isAftRadiusAutomatic();
68 // Implement the Radial interface:
70 public final double getOuterRadius(double x) {
76 public final double getRadius(double x, double theta) {
81 public final double getInnerRadius(double x, double theta) {
82 return getInnerRadius(x);
88 * Return the component wall thickness.
90 public double getThickness() {
92 return Math.max(getForeRadius(), getAftRadius());
93 return Math.min(thickness, Math.max(getForeRadius(), getAftRadius()));
98 * Set the component wall thickness. Values greater than the maximum radius are not
99 * allowed, and will result in setting the thickness to the maximum radius.
101 public void setThickness(double thickness) {
102 if ((this.thickness == thickness) && !filled)
104 this.thickness = MathUtil.clamp(thickness, 0, Math.max(getForeRadius(), getAftRadius()));
106 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
112 * Returns whether the component is set as filled. If it is set filled, then the
113 * wall thickness will have no effect.
115 public boolean isFilled() {
121 * Sets whether the component is set as filled. If the component is filled, then
122 * the wall thickness will have no effect.
124 public void setFilled(boolean filled) {
125 if (this.filled == filled)
127 this.filled = filled;
128 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
134 * Adds component bounds at a number of points between 0...length.
137 public Collection<Coordinate> getComponentBounds() {
138 List<Coordinate> list = new ArrayList<Coordinate>(20);
139 for (int n = 0; n <= 5; n++) {
140 double x = n * length / 5;
141 double r = getRadius(x);
142 addBound(list, x, r);
150 protected void loadFromPreset(ComponentPreset preset) {
151 if ( preset.has(ComponentPreset.THICKNESS) ) {
152 this.setThickness(preset.get(ComponentPreset.THICKNESS));
154 if ( preset.has(ComponentPreset.FILLED)) {
155 this.setFilled(preset.get(ComponentPreset.FILLED));
158 super.loadFromPreset(preset);
165 * Calculate volume of the component by integrating over the length of the component.
166 * The method caches the result, so subsequent calls are instant. Subclasses may
167 * override this method for simple shapes and use this method as necessary.
169 * @return The volume of the component.
172 public double getComponentVolume() {
180 * Calculate full (filled) volume of the component by integrating over the length
181 * of the component. The method caches the result, so subsequent calls are instant.
182 * Subclasses may override this method for simple shapes and use this method as
185 * @return The filled volume of the component.
187 public double getFullVolume() {
195 * Calculate the wetted area of the component by integrating over the length
196 * of the component. The method caches the result, so subsequent calls are instant.
197 * Subclasses may override this method for simple shapes and use this method as
200 * @return The wetted area of the component.
202 public double getComponentWetArea() {
210 * Calculate the planform area of the component by integrating over the length of
211 * the component. The method caches the result, so subsequent calls are instant.
212 * Subclasses may override this method for simple shapes and use this method as
215 * @return The planform area of the component.
217 public double getComponentPlanformArea() {
225 * Calculate the planform center X-coordinate of the component by integrating over
226 * the length of the component. The planform center is defined as
227 * <pre> integrate(x*2*r(x)) / planform area </pre>
228 * The method caches the result, so subsequent calls are instant. Subclasses may
229 * override this method for simple shapes and use this method as necessary.
231 * @return The planform center of the component.
233 public double getComponentPlanformCenter() {
241 * Calculate CG of the component by integrating over the length of the component.
242 * The method caches the result, so subsequent calls are instant. Subclasses may
243 * override this method for simple shapes and use this method as necessary.
245 * @return The CG+mass of the component.
248 public Coordinate getComponentCG() {
256 public double getLongitudinalUnitInertia() {
257 if (longitudinalInertia < 0) {
258 if (getComponentVolume() > 0.0000001) // == 0.1cm^3
259 integrateInertiaVolume();
261 integrateInertiaSurface();
263 return longitudinalInertia;
268 public double getRotationalUnitInertia() {
269 if (rotationalInertia < 0) {
270 if (getComponentVolume() > 0.0000001)
271 integrateInertiaVolume();
273 integrateInertiaSurface();
275 return rotationalInertia;
281 * Performs integration over the length of the component and updates the cached variables.
283 private void integrate() {
298 // Integrate for volume, CG, wetted area and planform area
300 final double l = length / DIVISIONS;
301 final double pil = Math.PI * l; // PI * l
302 final double pil3 = Math.PI * l / 3; // PI * l/3
312 for (int n = 1; n <= DIVISIONS; n++) {
314 * r1 and r2 are the two radii
315 * x is the position of r1
316 * hyp is the length of the hypotenuse from r1 to r2
317 * height if the y-axis height of the component if not filled
320 r2 = getRadius(x + l);
321 final double hyp = MathUtil.hypot(r2 - r1, l);
324 // Volume differential elements
328 dFullV = pil3 * (r1 * r1 + r1 * r2 + r2 * r2);
329 if (filled || r1 < thickness || r2 < thickness) {
334 final double height = thickness * hyp / l;
335 dV = MathUtil.max(pil * height * (r1 + r2 - height), 0);
338 // Add to the volume-related components
340 fullVolume += dFullV;
341 cgx += (x + l / 2) * dV;
343 // Wetted area ( * PI at the end)
344 wetArea += hyp * (r1 + r2);
346 // Planform area & center
347 final double p = l * (r1 + r2);
349 planCenter += (x + l / 2) * p;
351 // Update for next iteration
359 planCenter /= planArea;
361 if (volume < 0.0000000001) { // 0.1 mm^3
363 cg = new Coordinate(length / 2, 0, 0, 0);
365 // getComponentMass is safe now
366 // Use super.getComponentMass() to ensure only the transition shape mass
367 // is used, not the shoulders
368 cg = new Coordinate(cgx / volume, 0, 0, super.getComponentMass());
374 * Integrate the longitudinal and rotational inertia based on component volume.
375 * This method may be used only if the total volume is zero.
377 private void integrateInertiaVolume() {
380 final double l = length / DIVISIONS;
381 final double pil = Math.PI * l; // PI * l
382 final double pil3 = Math.PI * l / 3; // PI * l/3
386 longitudinalInertia = 0;
387 rotationalInertia = 0;
391 for (int n = 1; n <= DIVISIONS; n++) {
393 * r1 and r2 are the two radii, outer is their average
394 * x is the position of r1
395 * hyp is the length of the hypotenuse from r1 to r2
396 * height if the y-axis height of the component if not filled
398 r2 = getRadius(x + l);
399 final double outer = (r1 + r2) / 2;
402 // Volume differential elements
406 if (filled || r1 < thickness || r2 < thickness) {
408 dV = pil3 * (r1 * r1 + r1 * r2 + r2 * r2);
410 final double hyp = MathUtil.hypot(r2 - r1, l);
411 final double height = thickness * hyp / l;
412 dV = pil * height * (r1 + r2 - height);
413 inner = Math.max(outer - height, 0);
416 rotationalInertia += dV * (pow2(outer) + pow2(inner)) / 2;
417 longitudinalInertia += dV * ((3 * (pow2(outer) + pow2(inner)) + pow2(l)) / 12
422 // Update for next iteration
427 if (MathUtil.equals(vol, 0)) {
428 integrateInertiaSurface();
432 rotationalInertia /= vol;
433 longitudinalInertia /= vol;
435 // Shift longitudinal inertia to CG
436 longitudinalInertia = Math.max(longitudinalInertia - pow2(getComponentCG().x), 0);
442 * Integrate the longitudinal and rotational inertia based on component surface area.
443 * This method may be used only if the total volume is zero.
445 private void integrateInertiaSurface() {
448 final double l = length / DIVISIONS;
451 System.out.println(r1);
454 longitudinalInertia = 0;
455 rotationalInertia = 0;
459 for (int n = 1; n <= DIVISIONS; n++) {
461 * r1 and r2 are the two radii, outer is their average
462 * x is the position of r1
463 * hyp is the length of the hypotenuse from r1 to r2
464 * height if the y-axis height of the component if not filled
466 r2 = getRadius(x + l);
467 final double hyp = MathUtil.hypot(r2 - r1, l);
468 final double outer = (r1 + r2) / 2;
470 final double dS = hyp * (r1 + r2) * Math.PI;
472 rotationalInertia += dS * pow2(outer);
473 longitudinalInertia += dS * ((6 * pow2(outer) + pow2(l)) / 12 + pow2(x + l / 2));
477 // Update for next iteration
482 if (MathUtil.equals(surface, 0)) {
483 longitudinalInertia = 0;
484 rotationalInertia = 0;
488 longitudinalInertia /= surface;
489 rotationalInertia /= surface;
491 // Shift longitudinal inertia to CG
492 longitudinalInertia = Math.max(longitudinalInertia - pow2(getComponentCG().x), 0);
499 * Invalidates the cached volume and CG information.
502 protected void componentChanged(ComponentChangeEvent e) {
503 super.componentChanged(e);
504 if (!e.isOtherChange()) {
510 longitudinalInertia = -1;
511 rotationalInertia = -1;
518 /////////// Auto radius helper methods
522 * Returns the automatic radius for this component towards the
523 * front of the rocket. The automatics will not search towards the
524 * rear of the rocket for a suitable radius. A positive return value
525 * indicates a preferred radius, a negative value indicates that a
526 * match was not found.
528 protected abstract double getFrontAutoRadius();
531 * Returns the automatic radius for this component towards the
532 * end of the rocket. The automatics will not search towards the
533 * front of the rocket for a suitable radius. A positive return value
534 * indicates a preferred radius, a negative value indicates that a
535 * match was not found.
537 protected abstract double getRearAutoRadius();
542 * Return the previous symmetric component, or null if none exists.
543 * NOTE: This method currently assumes that there are no external
546 * @return the previous SymmetricComponent, or null.
548 protected final SymmetricComponent getPreviousSymmetricComponent() {
550 for (c = this.getPreviousComponent(); c != null; c = c.getPreviousComponent()) {
551 if (c instanceof SymmetricComponent) {
552 return (SymmetricComponent) c;
554 if (!(c instanceof Stage) &&
555 (c.relativePosition == RocketComponent.Position.AFTER))
556 return null; // Bad component type as "parent"
562 * Return the next symmetric component, or null if none exists.
563 * NOTE: This method currently assumes that there are no external
566 * @return the next SymmetricComponent, or null.
568 protected final SymmetricComponent getNextSymmetricComponent() {
570 for (c = this.getNextComponent(); c != null; c = c.getNextComponent()) {
571 if (c instanceof SymmetricComponent) {
572 return (SymmetricComponent) c;
574 if (!(c instanceof Stage) &&
575 (c.relativePosition == RocketComponent.Position.AFTER))
576 return null; // Bad component type as "parent"