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 step = length / DIVISIONS;
302 final double pi3 = Math.PI / 3.0;
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 * l is the step size for the current loop. Could also be called delta-x.
322 * to account for accumulated errors in the x position during the loop
323 * during the last iteration (n== DIVISIONS) we recompute l to be
326 double l = (n==DIVISIONS) ? length -x : step;
328 // Further to prevent round off error from the previous statement,
329 // we clamp r2 to length at the last iteration.
330 r2 = getRadius((n==DIVISIONS) ? length : x + l);
332 final double hyp = MathUtil.hypot(r2 - r1, l);
334 // Volume differential elements
338 dFullV = pi3 * l * (r1 * r1 + r1 * r2 + r2 * r2);
344 // Thickness is normal to the surface of the component
345 // here we use simple trig to project the Thickness
346 // on to the y dimension (radius).
347 double height = thickness * hyp / l;
348 if (r1 < height || r2 < height) {
349 // Filled portion of piece
352 // Hollow portion of piece
353 dV = MathUtil.max(Math.PI* l * height * (r1 + r2 - height), 0);
357 // Add to the volume-related components
359 fullVolume += dFullV;
360 cgx += (x + l / 2) * dV;
362 // Wetted area ( * PI at the end)
363 wetArea += hyp * (r1 + r2);
365 // Planform area & center
366 final double p = l * (r1 + r2);
368 planCenter += (x + l / 2) * p;
370 // Update for next iteration
378 planCenter /= planArea;
380 if (volume < 0.0000000001) { // 0.1 mm^3
382 cg = new Coordinate(length / 2, 0, 0, 0);
384 // the mass of this shape is the material density * volume.
385 // it cannot come from super.getComponentMass() since that
386 // includes the shoulders
387 cg = new Coordinate(cgx / volume, 0, 0, getMaterial().getDensity() * volume );
393 * Integrate the longitudinal and rotational inertia based on component volume.
394 * This method may be used only if the total volume is zero.
396 private void integrateInertiaVolume() {
399 final double l = length / DIVISIONS;
400 final double pil = Math.PI * l; // PI * l
401 final double pil3 = Math.PI * l / 3; // PI * l/3
405 longitudinalInertia = 0;
406 rotationalInertia = 0;
410 for (int n = 1; n <= DIVISIONS; n++) {
412 * r1 and r2 are the two radii, outer is their average
413 * x is the position of r1
414 * hyp is the length of the hypotenuse from r1 to r2
415 * height if the y-axis height of the component if not filled
417 r2 = getRadius(x + l);
418 final double outer = (r1 + r2) / 2;
421 // Volume differential elements
425 if (filled || r1 < thickness || r2 < thickness) {
427 dV = pil3 * (r1 * r1 + r1 * r2 + r2 * r2);
429 final double hyp = MathUtil.hypot(r2 - r1, l);
430 final double height = thickness * hyp / l;
431 dV = pil * height * (r1 + r2 - height);
432 inner = Math.max(outer - height, 0);
435 rotationalInertia += dV * (pow2(outer) + pow2(inner)) / 2;
436 longitudinalInertia += dV * ((3 * (pow2(outer) + pow2(inner)) + pow2(l)) / 12
441 // Update for next iteration
446 if (MathUtil.equals(vol, 0)) {
447 integrateInertiaSurface();
451 rotationalInertia /= vol;
452 longitudinalInertia /= vol;
454 // Shift longitudinal inertia to CG
455 longitudinalInertia = Math.max(longitudinalInertia - pow2(getComponentCG().x), 0);
461 * Integrate the longitudinal and rotational inertia based on component surface area.
462 * This method may be used only if the total volume is zero.
464 private void integrateInertiaSurface() {
467 final double l = length / DIVISIONS;
470 System.out.println(r1);
473 longitudinalInertia = 0;
474 rotationalInertia = 0;
478 for (int n = 1; n <= DIVISIONS; n++) {
480 * r1 and r2 are the two radii, outer is their average
481 * x is the position of r1
482 * hyp is the length of the hypotenuse from r1 to r2
483 * height if the y-axis height of the component if not filled
485 r2 = getRadius(x + l);
486 final double hyp = MathUtil.hypot(r2 - r1, l);
487 final double outer = (r1 + r2) / 2;
489 final double dS = hyp * (r1 + r2) * Math.PI;
491 rotationalInertia += dS * pow2(outer);
492 longitudinalInertia += dS * ((6 * pow2(outer) + pow2(l)) / 12 + pow2(x + l / 2));
496 // Update for next iteration
501 if (MathUtil.equals(surface, 0)) {
502 longitudinalInertia = 0;
503 rotationalInertia = 0;
507 longitudinalInertia /= surface;
508 rotationalInertia /= surface;
510 // Shift longitudinal inertia to CG
511 longitudinalInertia = Math.max(longitudinalInertia - pow2(getComponentCG().x), 0);
518 * Invalidates the cached volume and CG information.
521 protected void componentChanged(ComponentChangeEvent e) {
522 super.componentChanged(e);
523 if (!e.isOtherChange()) {
529 longitudinalInertia = -1;
530 rotationalInertia = -1;
537 /////////// Auto radius helper methods
541 * Returns the automatic radius for this component towards the
542 * front of the rocket. The automatics will not search towards the
543 * rear of the rocket for a suitable radius. A positive return value
544 * indicates a preferred radius, a negative value indicates that a
545 * match was not found.
547 protected abstract double getFrontAutoRadius();
550 * Returns the automatic radius for this component towards the
551 * end of the rocket. The automatics will not search towards the
552 * front of the rocket for a suitable radius. A positive return value
553 * indicates a preferred radius, a negative value indicates that a
554 * match was not found.
556 protected abstract double getRearAutoRadius();
561 * Return the previous symmetric component, or null if none exists.
562 * NOTE: This method currently assumes that there are no external
565 * @return the previous SymmetricComponent, or null.
567 protected final SymmetricComponent getPreviousSymmetricComponent() {
569 for (c = this.getPreviousComponent(); c != null; c = c.getPreviousComponent()) {
570 if (c instanceof SymmetricComponent) {
571 return (SymmetricComponent) c;
573 if (!(c instanceof Stage) &&
574 (c.relativePosition == RocketComponent.Position.AFTER))
575 return null; // Bad component type as "parent"
581 * Return the next symmetric component, or null if none exists.
582 * NOTE: This method currently assumes that there are no external
585 * @return the next SymmetricComponent, or null.
587 protected final SymmetricComponent getNextSymmetricComponent() {
589 for (c = this.getNextComponent(); c != null; c = c.getNextComponent()) {
590 if (c instanceof SymmetricComponent) {
591 return (SymmetricComponent) c;
593 if (!(c instanceof Stage) &&
594 (c.relativePosition == RocketComponent.Position.AFTER))
595 return null; // Bad component type as "parent"