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.thickness = preset.get(ComponentPreset.THICKNESS);
155 if ( preset.has(ComponentPreset.FILLED)) {
159 super.loadFromPreset(preset);
166 * Calculate volume of the component by integrating over the length of the component.
167 * The method caches the result, so subsequent calls are instant. Subclasses may
168 * override this method for simple shapes and use this method as necessary.
170 * @return The volume of the component.
173 public double getComponentVolume() {
181 * Calculate full (filled) volume of the component by integrating over the length
182 * of the component. The method caches the result, so subsequent calls are instant.
183 * Subclasses may override this method for simple shapes and use this method as
186 * @return The filled volume of the component.
188 public double getFullVolume() {
196 * Calculate the wetted area of the component by integrating over the length
197 * of the component. The method caches the result, so subsequent calls are instant.
198 * Subclasses may override this method for simple shapes and use this method as
201 * @return The wetted area of the component.
203 public double getComponentWetArea() {
211 * Calculate the planform area of the component by integrating over the length of
212 * the component. The method caches the result, so subsequent calls are instant.
213 * Subclasses may override this method for simple shapes and use this method as
216 * @return The planform area of the component.
218 public double getComponentPlanformArea() {
226 * Calculate the planform center X-coordinate of the component by integrating over
227 * the length of the component. The planform center is defined as
228 * <pre> integrate(x*2*r(x)) / planform area </pre>
229 * The method caches the result, so subsequent calls are instant. Subclasses may
230 * override this method for simple shapes and use this method as necessary.
232 * @return The planform center of the component.
234 public double getComponentPlanformCenter() {
242 * Calculate CG of the component by integrating over the length of the component.
243 * The method caches the result, so subsequent calls are instant. Subclasses may
244 * override this method for simple shapes and use this method as necessary.
246 * @return The CG+mass of the component.
249 public Coordinate getComponentCG() {
257 public double getLongitudinalUnitInertia() {
258 if (longitudinalInertia < 0) {
259 if (getComponentVolume() > 0.0000001) // == 0.1cm^3
260 integrateInertiaVolume();
262 integrateInertiaSurface();
264 return longitudinalInertia;
269 public double getRotationalUnitInertia() {
270 if (rotationalInertia < 0) {
271 if (getComponentVolume() > 0.0000001)
272 integrateInertiaVolume();
274 integrateInertiaSurface();
276 return rotationalInertia;
282 * Performs integration over the length of the component and updates the cached variables.
284 private void integrate() {
299 // Integrate for volume, CG, wetted area and planform area
301 final double l = length / DIVISIONS;
302 final double pil = Math.PI * l; // PI * l
303 final double pil3 = Math.PI * l / 3; // PI * l/3
313 for (int n = 1; n <= DIVISIONS; n++) {
315 * r1 and r2 are the two radii
316 * x is the position of r1
317 * hyp is the length of the hypotenuse from r1 to r2
318 * height if the y-axis height of the component if not filled
321 r2 = getRadius(x + l);
322 final double hyp = MathUtil.hypot(r2 - r1, l);
325 // Volume differential elements
329 dFullV = pil3 * (r1 * r1 + r1 * r2 + r2 * r2);
330 if (filled || r1 < thickness || r2 < thickness) {
335 final double height = thickness * hyp / l;
336 dV = MathUtil.max(pil * height * (r1 + r2 - height), 0);
339 // Add to the volume-related components
341 fullVolume += dFullV;
342 cgx += (x + l / 2) * dV;
344 // Wetted area ( * PI at the end)
345 wetArea += hyp * (r1 + r2);
347 // Planform area & center
348 final double p = l * (r1 + r2);
350 planCenter += (x + l / 2) * p;
352 // Update for next iteration
360 planCenter /= planArea;
362 if (volume < 0.0000000001) { // 0.1 mm^3
364 cg = new Coordinate(length / 2, 0, 0, 0);
366 // getComponentMass is safe now
367 // Use super.getComponentMass() to ensure only the transition shape mass
368 // is used, not the shoulders
369 cg = new Coordinate(cgx / volume, 0, 0, super.getComponentMass());
375 * Integrate the longitudinal and rotational inertia based on component volume.
376 * This method may be used only if the total volume is zero.
378 private void integrateInertiaVolume() {
381 final double l = length / DIVISIONS;
382 final double pil = Math.PI * l; // PI * l
383 final double pil3 = Math.PI * l / 3; // PI * l/3
387 longitudinalInertia = 0;
388 rotationalInertia = 0;
392 for (int n = 1; n <= DIVISIONS; n++) {
394 * r1 and r2 are the two radii, outer is their average
395 * x is the position of r1
396 * hyp is the length of the hypotenuse from r1 to r2
397 * height if the y-axis height of the component if not filled
399 r2 = getRadius(x + l);
400 final double outer = (r1 + r2) / 2;
403 // Volume differential elements
407 if (filled || r1 < thickness || r2 < thickness) {
409 dV = pil3 * (r1 * r1 + r1 * r2 + r2 * r2);
411 final double hyp = MathUtil.hypot(r2 - r1, l);
412 final double height = thickness * hyp / l;
413 dV = pil * height * (r1 + r2 - height);
414 inner = Math.max(outer - height, 0);
417 rotationalInertia += dV * (pow2(outer) + pow2(inner)) / 2;
418 longitudinalInertia += dV * ((3 * (pow2(outer) + pow2(inner)) + pow2(l)) / 12
423 // Update for next iteration
428 if (MathUtil.equals(vol, 0)) {
429 integrateInertiaSurface();
433 rotationalInertia /= vol;
434 longitudinalInertia /= vol;
436 // Shift longitudinal inertia to CG
437 longitudinalInertia = Math.max(longitudinalInertia - pow2(getComponentCG().x), 0);
443 * Integrate the longitudinal and rotational inertia based on component surface area.
444 * This method may be used only if the total volume is zero.
446 private void integrateInertiaSurface() {
449 final double l = length / DIVISIONS;
452 System.out.println(r1);
455 longitudinalInertia = 0;
456 rotationalInertia = 0;
460 for (int n = 1; n <= DIVISIONS; n++) {
462 * r1 and r2 are the two radii, outer is their average
463 * x is the position of r1
464 * hyp is the length of the hypotenuse from r1 to r2
465 * height if the y-axis height of the component if not filled
467 r2 = getRadius(x + l);
468 final double hyp = MathUtil.hypot(r2 - r1, l);
469 final double outer = (r1 + r2) / 2;
471 final double dS = hyp * (r1 + r2) * Math.PI;
473 rotationalInertia += dS * pow2(outer);
474 longitudinalInertia += dS * ((6 * pow2(outer) + pow2(l)) / 12 + pow2(x + l / 2));
478 // Update for next iteration
483 if (MathUtil.equals(surface, 0)) {
484 longitudinalInertia = 0;
485 rotationalInertia = 0;
489 longitudinalInertia /= surface;
490 rotationalInertia /= surface;
492 // Shift longitudinal inertia to CG
493 longitudinalInertia = Math.max(longitudinalInertia - pow2(getComponentCG().x), 0);
500 * Invalidates the cached volume and CG information.
503 protected void componentChanged(ComponentChangeEvent e) {
504 super.componentChanged(e);
505 if (!e.isOtherChange()) {
511 longitudinalInertia = -1;
512 rotationalInertia = -1;
519 /////////// Auto radius helper methods
523 * Returns the automatic radius for this component towards the
524 * front of the rocket. The automatics will not search towards the
525 * rear of the rocket for a suitable radius. A positive return value
526 * indicates a preferred radius, a negative value indicates that a
527 * match was not found.
529 protected abstract double getFrontAutoRadius();
532 * Returns the automatic radius for this component towards the
533 * end of the rocket. The automatics will not search towards the
534 * front of the rocket for a suitable radius. A positive return value
535 * indicates a preferred radius, a negative value indicates that a
536 * match was not found.
538 protected abstract double getRearAutoRadius();
543 * Return the previous symmetric component, or null if none exists.
544 * NOTE: This method currently assumes that there are no external
547 * @return the previous SymmetricComponent, or null.
549 protected final SymmetricComponent getPreviousSymmetricComponent() {
551 for (c = this.getPreviousComponent(); c != null; c = c.getPreviousComponent()) {
552 if (c instanceof SymmetricComponent) {
553 return (SymmetricComponent) c;
555 if (!(c instanceof Stage) &&
556 (c.relativePosition == RocketComponent.Position.AFTER))
557 return null; // Bad component type as "parent"
563 * Return the next symmetric component, or null if none exists.
564 * NOTE: This method currently assumes that there are no external
567 * @return the next SymmetricComponent, or null.
569 protected final SymmetricComponent getNextSymmetricComponent() {
571 for (c = this.getNextComponent(); c != null; c = c.getNextComponent()) {
572 if (c instanceof SymmetricComponent) {
573 return (SymmetricComponent) c;
575 if (!(c instanceof Stage) &&
576 (c.relativePosition == RocketComponent.Position.AFTER))
577 return null; // Bad component type as "parent"