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);
111 * Returns whether the component is set as filled. If it is set filled, then the
112 * wall thickness will have no effect.
114 public boolean isFilled() {
120 * Sets whether the component is set as filled. If the component is filled, then
121 * the wall thickness will have no effect.
123 public void setFilled(boolean filled) {
124 if (this.filled == filled)
126 this.filled = filled;
127 fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
133 * Adds component bounds at a number of points between 0...length.
136 public Collection<Coordinate> getComponentBounds() {
137 List<Coordinate> list = new ArrayList<Coordinate>(20);
138 for (int n = 0; n <= 5; n++) {
139 double x = n * length / 5;
140 double r = getRadius(x);
141 addBound(list, x, r);
149 protected void loadFromPreset(RocketComponent preset) {
150 SymmetricComponent c = (SymmetricComponent) preset;
151 this.setThickness(c.getThickness());
152 this.setFilled(c.isFilled());
154 super.loadFromPreset(preset);
161 * Calculate volume of the component by integrating over the length of the component.
162 * The method caches the result, so subsequent calls are instant. Subclasses may
163 * override this method for simple shapes and use this method as necessary.
165 * @return The volume of the component.
168 public double getComponentVolume() {
176 * Calculate full (filled) volume of the component by integrating over the length
177 * of the component. The method caches the result, so subsequent calls are instant.
178 * Subclasses may override this method for simple shapes and use this method as
181 * @return The filled volume of the component.
183 public double getFullVolume() {
191 * Calculate the wetted area of the component by integrating over the length
192 * of the component. The method caches the result, so subsequent calls are instant.
193 * Subclasses may override this method for simple shapes and use this method as
196 * @return The wetted area of the component.
198 public double getComponentWetArea() {
206 * Calculate the planform area of the component by integrating over the length of
207 * the component. The method caches the result, so subsequent calls are instant.
208 * Subclasses may override this method for simple shapes and use this method as
211 * @return The planform area of the component.
213 public double getComponentPlanformArea() {
221 * Calculate the planform center X-coordinate of the component by integrating over
222 * the length of the component. The planform center is defined as
223 * <pre> integrate(x*2*r(x)) / planform area </pre>
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 planform center of the component.
229 public double getComponentPlanformCenter() {
237 * Calculate CG of the component by integrating over the length of the component.
238 * The method caches the result, so subsequent calls are instant. Subclasses may
239 * override this method for simple shapes and use this method as necessary.
241 * @return The CG+mass of the component.
244 public Coordinate getComponentCG() {
252 public double getLongitudinalUnitInertia() {
253 if (longitudinalInertia < 0) {
254 if (getComponentVolume() > 0.0000001) // == 0.1cm^3
255 integrateInertiaVolume();
257 integrateInertiaSurface();
259 return longitudinalInertia;
264 public double getRotationalUnitInertia() {
265 if (rotationalInertia < 0) {
266 if (getComponentVolume() > 0.0000001)
267 integrateInertiaVolume();
269 integrateInertiaSurface();
271 return rotationalInertia;
277 * Performs integration over the length of the component and updates the cached variables.
279 private void integrate() {
294 // Integrate for volume, CG, wetted area and planform area
296 final double l = length / DIVISIONS;
297 final double pil = Math.PI * l; // PI * l
298 final double pil3 = Math.PI * l / 3; // PI * l/3
308 for (int n = 1; n <= DIVISIONS; n++) {
310 * r1 and r2 are the two radii
311 * x is the position of r1
312 * hyp is the length of the hypotenuse from r1 to r2
313 * height if the y-axis height of the component if not filled
316 r2 = getRadius(x + l);
317 final double hyp = MathUtil.hypot(r2 - r1, l);
320 // Volume differential elements
324 dFullV = pil3 * (r1 * r1 + r1 * r2 + r2 * r2);
325 if (filled || r1 < thickness || r2 < thickness) {
330 final double height = thickness * hyp / l;
331 dV = MathUtil.max(pil * height * (r1 + r2 - height), 0);
334 // Add to the volume-related components
336 fullVolume += dFullV;
337 cgx += (x + l / 2) * dV;
339 // Wetted area ( * PI at the end)
340 wetArea += hyp * (r1 + r2);
342 // Planform area & center
343 final double p = l * (r1 + r2);
345 planCenter += (x + l / 2) * p;
347 // Update for next iteration
355 planCenter /= planArea;
357 if (volume < 0.0000000001) { // 0.1 mm^3
359 cg = new Coordinate(length / 2, 0, 0, 0);
361 // getComponentMass is safe now
362 // Use super.getComponentMass() to ensure only the transition shape mass
363 // is used, not the shoulders
364 cg = new Coordinate(cgx / volume, 0, 0, super.getComponentMass());
370 * Integrate the longitudinal and rotational inertia based on component volume.
371 * This method may be used only if the total volume is zero.
373 private void integrateInertiaVolume() {
376 final double l = length / DIVISIONS;
377 final double pil = Math.PI * l; // PI * l
378 final double pil3 = Math.PI * l / 3; // PI * l/3
382 longitudinalInertia = 0;
383 rotationalInertia = 0;
387 for (int n = 1; n <= DIVISIONS; n++) {
389 * r1 and r2 are the two radii, outer is their average
390 * x is the position of r1
391 * hyp is the length of the hypotenuse from r1 to r2
392 * height if the y-axis height of the component if not filled
394 r2 = getRadius(x + l);
395 final double outer = (r1 + r2) / 2;
398 // Volume differential elements
402 if (filled || r1 < thickness || r2 < thickness) {
404 dV = pil3 * (r1 * r1 + r1 * r2 + r2 * r2);
406 final double hyp = MathUtil.hypot(r2 - r1, l);
407 final double height = thickness * hyp / l;
408 dV = pil * height * (r1 + r2 - height);
409 inner = Math.max(outer - height, 0);
412 rotationalInertia += dV * (pow2(outer) + pow2(inner)) / 2;
413 longitudinalInertia += dV * ((3 * (pow2(outer) + pow2(inner)) + pow2(l)) / 12
418 // Update for next iteration
423 if (MathUtil.equals(vol, 0)) {
424 integrateInertiaSurface();
428 rotationalInertia /= vol;
429 longitudinalInertia /= vol;
431 // Shift longitudinal inertia to CG
432 longitudinalInertia = Math.max(longitudinalInertia - pow2(getComponentCG().x), 0);
438 * Integrate the longitudinal and rotational inertia based on component surface area.
439 * This method may be used only if the total volume is zero.
441 private void integrateInertiaSurface() {
444 final double l = length / DIVISIONS;
447 System.out.println(r1);
450 longitudinalInertia = 0;
451 rotationalInertia = 0;
455 for (int n = 1; n <= DIVISIONS; n++) {
457 * r1 and r2 are the two radii, outer is their average
458 * x is the position of r1
459 * hyp is the length of the hypotenuse from r1 to r2
460 * height if the y-axis height of the component if not filled
462 r2 = getRadius(x + l);
463 final double hyp = MathUtil.hypot(r2 - r1, l);
464 final double outer = (r1 + r2) / 2;
466 final double dS = hyp * (r1 + r2) * Math.PI;
468 rotationalInertia += dS * pow2(outer);
469 longitudinalInertia += dS * ((6 * pow2(outer) + pow2(l)) / 12 + pow2(x + l / 2));
473 // Update for next iteration
478 if (MathUtil.equals(surface, 0)) {
479 longitudinalInertia = 0;
480 rotationalInertia = 0;
484 longitudinalInertia /= surface;
485 rotationalInertia /= surface;
487 // Shift longitudinal inertia to CG
488 longitudinalInertia = Math.max(longitudinalInertia - pow2(getComponentCG().x), 0);
495 * Invalidates the cached volume and CG information.
498 protected void componentChanged(ComponentChangeEvent e) {
499 super.componentChanged(e);
500 if (!e.isOtherChange()) {
506 longitudinalInertia = -1;
507 rotationalInertia = -1;
514 /////////// Auto radius helper methods
518 * Returns the automatic radius for this component towards the
519 * front of the rocket. The automatics will not search towards the
520 * rear of the rocket for a suitable radius. A positive return value
521 * indicates a preferred radius, a negative value indicates that a
522 * match was not found.
524 protected abstract double getFrontAutoRadius();
527 * Returns the automatic radius for this component towards the
528 * end of the rocket. The automatics will not search towards the
529 * front of the rocket for a suitable radius. A positive return value
530 * indicates a preferred radius, a negative value indicates that a
531 * match was not found.
533 protected abstract double getRearAutoRadius();
538 * Return the previous symmetric component, or null if none exists.
539 * NOTE: This method currently assumes that there are no external
542 * @return the previous SymmetricComponent, or null.
544 protected final SymmetricComponent getPreviousSymmetricComponent() {
546 for (c = this.getPreviousComponent(); c != null; c = c.getPreviousComponent()) {
547 if (c instanceof SymmetricComponent) {
548 return (SymmetricComponent) c;
550 if (!(c instanceof Stage) &&
551 (c.relativePosition == RocketComponent.Position.AFTER))
552 return null; // Bad component type as "parent"
558 * Return the next symmetric component, or null if none exists.
559 * NOTE: This method currently assumes that there are no external
562 * @return the next SymmetricComponent, or null.
564 protected final SymmetricComponent getNextSymmetricComponent() {
566 for (c = this.getNextComponent(); c != null; c = c.getNextComponent()) {
567 if (c instanceof SymmetricComponent) {
568 return (SymmetricComponent) c;
570 if (!(c instanceof Stage) &&
571 (c.relativePosition == RocketComponent.Position.AFTER))
572 return null; // Bad component type as "parent"