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 longitudinalInertia = -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);
56 public abstract double getInnerRadius(double x);
58 public abstract double getForeRadius();
60 public abstract boolean isForeRadiusAutomatic();
62 public abstract double getAftRadius();
64 public abstract boolean isAftRadiusAutomatic();
67 // Implement the Radial interface:
69 public final double getOuterRadius(double x) {
75 public final double getRadius(double x, double theta) {
80 public final double getInnerRadius(double x, double theta) {
81 return getInnerRadius(x);
87 * Return the component wall thickness.
89 public double getThickness() {
91 return Math.max(getForeRadius(), getAftRadius());
92 return Math.min(thickness, Math.max(getForeRadius(), getAftRadius()));
97 * Set the component wall thickness. Values greater than the maximum radius are not
98 * allowed, and will result in setting the thickness to the maximum radius.
100 public void setThickness(double thickness) {
101 if ((this.thickness == thickness) && !filled)
103 this.thickness = MathUtil.clamp(thickness, 0, Math.max(getForeRadius(), getAftRadius()));
105 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
110 * Returns whether the component is set as filled. If it is set filled, then the
111 * wall thickness will have no effect.
113 public boolean isFilled() {
119 * Sets whether the component is set as filled. If the component is filled, then
120 * the wall thickness will have no effect.
122 public void setFilled(boolean filled) {
123 if (this.filled == filled)
125 this.filled = filled;
126 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
131 * Adds component bounds at a number of points between 0...length.
134 public Collection<Coordinate> getComponentBounds() {
135 List<Coordinate> list = new ArrayList<Coordinate>(20);
136 for (int n = 0; n <= 5; n++) {
137 double x = n * length / 5;
138 double r = getRadius(x);
139 addBound(list, x, r);
147 * Calculate volume of the component by integrating over the length of the component.
148 * The method caches the result, so subsequent calls are instant. Subclasses may
149 * override this method for simple shapes and use this method as necessary.
151 * @return The volume of the component.
154 public double getComponentVolume() {
162 * Calculate full (filled) volume of the component by integrating over the length
163 * of the component. The method caches the result, so subsequent calls are instant.
164 * Subclasses may override this method for simple shapes and use this method as
167 * @return The filled volume of the component.
169 public double getFullVolume() {
177 * Calculate the wetted area of the component by integrating over the length
178 * of the component. The method caches the result, so subsequent calls are instant.
179 * Subclasses may override this method for simple shapes and use this method as
182 * @return The wetted area of the component.
184 public double getComponentWetArea() {
192 * Calculate the planform area of the component by integrating over the length of
193 * the component. The method caches the result, so subsequent calls are instant.
194 * Subclasses may override this method for simple shapes and use this method as
197 * @return The planform area of the component.
199 public double getComponentPlanformArea() {
207 * Calculate the planform center X-coordinate of the component by integrating over
208 * the length of the component. The planform center is defined as
209 * <pre> integrate(x*2*r(x)) / planform area </pre>
210 * The method caches the result, so subsequent calls are instant. Subclasses may
211 * override this method for simple shapes and use this method as necessary.
213 * @return The planform center of the component.
215 public double getComponentPlanformCenter() {
223 * Calculate CG of the component by integrating over the length of the component.
224 * The method caches the result, so subsequent calls are instant. Subclasses may
225 * override this method for simple shapes and use this method as necessary.
227 * @return The CG+mass of the component.
230 public Coordinate getComponentCG() {
238 public double getLongitudinalUnitInertia() {
239 if (longitudinalInertia < 0) {
240 if (getComponentVolume() > 0.0000001) // == 0.1cm^3
241 integrateInertiaVolume();
243 integrateInertiaSurface();
245 return longitudinalInertia;
250 public double getRotationalUnitInertia() {
251 if (rotationalInertia < 0) {
252 if (getComponentVolume() > 0.0000001)
253 integrateInertiaVolume();
255 integrateInertiaSurface();
257 return rotationalInertia;
263 * Performs integration over the length of the component and updates the cached variables.
265 private void integrate() {
280 // Integrate for volume, CG, wetted area and planform area
282 final double l = length / DIVISIONS;
283 final double pil = Math.PI * l; // PI * l
284 final double pil3 = Math.PI * l / 3; // PI * l/3
294 for (int n = 1; n <= DIVISIONS; n++) {
296 * r1 and r2 are the two radii
297 * x is the position of r1
298 * hyp is the length of the hypotenuse from r1 to r2
299 * height if the y-axis height of the component if not filled
302 r2 = getRadius(x + l);
303 final double hyp = MathUtil.hypot(r2 - r1, l);
306 // Volume differential elements
310 dFullV = pil3 * (r1 * r1 + r1 * r2 + r2 * r2);
311 if (filled || r1 < thickness || r2 < thickness) {
316 final double height = thickness * hyp / l;
317 dV = MathUtil.max(pil * height * (r1 + r2 - height), 0);
320 // Add to the volume-related components
322 fullVolume += dFullV;
323 cgx += (x + l / 2) * dV;
325 // Wetted area ( * PI at the end)
326 wetArea += hyp * (r1 + r2);
328 // Planform area & center
329 final double p = l * (r1 + r2);
331 planCenter += (x + l / 2) * p;
333 // Update for next iteration
341 planCenter /= planArea;
343 if (volume < 0.0000000001) { // 0.1 mm^3
345 cg = new Coordinate(length / 2, 0, 0, 0);
347 // getComponentMass is safe now
348 // Use super.getComponentMass() to ensure only the transition shape mass
349 // is used, not the shoulders
350 cg = new Coordinate(cgx / volume, 0, 0, super.getComponentMass());
356 * Integrate the longitudinal and rotational inertia based on component volume.
357 * This method may be used only if the total volume is zero.
359 private void integrateInertiaVolume() {
362 final double l = length / DIVISIONS;
363 final double pil = Math.PI * l; // PI * l
364 final double pil3 = Math.PI * l / 3; // PI * l/3
368 longitudinalInertia = 0;
369 rotationalInertia = 0;
373 for (int n = 1; n <= DIVISIONS; n++) {
375 * r1 and r2 are the two radii, outer is their average
376 * x is the position of r1
377 * hyp is the length of the hypotenuse from r1 to r2
378 * height if the y-axis height of the component if not filled
380 r2 = getRadius(x + l);
381 final double outer = (r1 + r2) / 2;
384 // Volume differential elements
388 if (filled || r1 < thickness || r2 < thickness) {
390 dV = pil3 * (r1 * r1 + r1 * r2 + r2 * r2);
392 final double hyp = MathUtil.hypot(r2 - r1, l);
393 final double height = thickness * hyp / l;
394 dV = pil * height * (r1 + r2 - height);
395 inner = Math.max(outer - height, 0);
398 rotationalInertia += dV * (pow2(outer) + pow2(inner)) / 2;
399 longitudinalInertia += dV * ((3 * (pow2(outer) + pow2(inner)) + pow2(l)) / 12
404 // Update for next iteration
409 if (MathUtil.equals(volume, 0)) {
410 integrateInertiaSurface();
414 rotationalInertia /= volume;
415 longitudinalInertia /= volume;
417 // Shift longitudinal inertia to CG
418 longitudinalInertia = Math.max(longitudinalInertia - pow2(getComponentCG().x), 0);
424 * Integrate the longitudinal and rotational inertia based on component surface area.
425 * This method may be used only if the total volume is zero.
427 private void integrateInertiaSurface() {
430 final double l = length / DIVISIONS;
433 System.out.println(r1);
436 longitudinalInertia = 0;
437 rotationalInertia = 0;
441 for (int n = 1; n <= DIVISIONS; n++) {
443 * r1 and r2 are the two radii, outer is their average
444 * x is the position of r1
445 * hyp is the length of the hypotenuse from r1 to r2
446 * height if the y-axis height of the component if not filled
448 r2 = getRadius(x + l);
449 final double hyp = MathUtil.hypot(r2 - r1, l);
450 final double outer = (r1 + r2) / 2;
452 final double dS = hyp * (r1 + r2) * Math.PI;
454 rotationalInertia += dS * pow2(outer);
455 longitudinalInertia += dS * ((6 * pow2(outer) + pow2(l)) / 12 + pow2(x + l / 2));
459 // Update for next iteration
464 if (MathUtil.equals(surface, 0)) {
465 longitudinalInertia = 0;
466 rotationalInertia = 0;
470 longitudinalInertia /= surface;
471 rotationalInertia /= surface;
473 // Shift longitudinal inertia to CG
474 longitudinalInertia = Math.max(longitudinalInertia - pow2(getComponentCG().x), 0);
481 * Invalidates the cached volume and CG information.
484 protected void componentChanged(ComponentChangeEvent e) {
485 super.componentChanged(e);
486 if (!e.isOtherChange()) {
492 longitudinalInertia = -1;
493 rotationalInertia = -1;
500 /////////// Auto radius helper methods
504 * Returns the automatic radius for this component towards the
505 * front of the rocket. The automatics will not search towards the
506 * rear of the rocket for a suitable radius. A positive return value
507 * indicates a preferred radius, a negative value indicates that a
508 * match was not found.
510 protected abstract double getFrontAutoRadius();
513 * Returns the automatic radius for this component towards the
514 * end of the rocket. The automatics will not search towards the
515 * front of the rocket for a suitable radius. A positive return value
516 * indicates a preferred radius, a negative value indicates that a
517 * match was not found.
519 protected abstract double getRearAutoRadius();
524 * Return the previous symmetric component, or null if none exists.
525 * NOTE: This method currently assumes that there are no external
528 * @return the previous SymmetricComponent, or null.
530 protected final SymmetricComponent getPreviousSymmetricComponent() {
532 for (c = this.getPreviousComponent(); c != null; c = c.getPreviousComponent()) {
533 if (c instanceof SymmetricComponent) {
534 return (SymmetricComponent) c;
536 if (!(c instanceof Stage) &&
537 (c.relativePosition == RocketComponent.Position.AFTER))
538 return null; // Bad component type as "parent"
544 * Return the next symmetric component, or null if none exists.
545 * NOTE: This method currently assumes that there are no external
548 * @return the next SymmetricComponent, or null.
550 protected final SymmetricComponent getNextSymmetricComponent() {
552 for (c = this.getNextComponent(); c != null; c = c.getNextComponent()) {
553 if (c instanceof SymmetricComponent) {
554 return (SymmetricComponent) c;
556 if (!(c instanceof Stage) &&
557 (c.relativePosition == RocketComponent.Position.AFTER))
558 return null; // Bad component type as "parent"